What you will learn
This chapter adds text to your game: score, lives, level, and end-of-game messages. You will discover Pygame's font system, how to display text on screen, and how to create a complete heads-up display (HUD).
The concept
In Pygame, text is not drawn directly: it is first converted into a Surface (an image), then blitted onto the screen. The two-step process is:
- Create a font:
pygame.font.SysFont(name, size)loads a font installed on the system. - Render the text:
font.render(text, antialias, color)creates a Surface containing the text.
The antialias parameter (True/False) enables edge smoothing. Always use True for a pleasant look.
To center text, use surface.get_width() to know its pixel width, then calculate the position:
x = SCREEN_WIDTH // 2 - surface.get_width() // 2
Game states
This chapter also introduces the concept of game states. Our game now has three possible states:
STATE_PLAYING: the game is runningSTATE_GAME_OVER: the player lost all livesSTATE_YOU_WIN: all bricks are destroyed
Only the PLAYING state updates game logic. The others display an end screen and wait for the player to press R to restart.
Step by step
1. Creating fonts
We create several fonts of different sizes for each use case: HUD, titles, messages.
hud_font = pygame.font.SysFont("monospace", 18)
big_font = pygame.font.SysFont("monospace", 48, bold=True)
medium_font = pygame.font.SysFont("monospace", 24)
small_font = pygame.font.SysFont("monospace", 14)
SysFont uses fonts installed on your system. "monospace" is available everywhere. The bold=True parameter enables bold text.
2. Displaying score, lives, and level
We draw the HUD (Head-Up Display) at the top of the screen each frame. Score is on the left, lives in the center, level on the right.
# HUD
score_surf = hud_font.render(f"Score: {score}", True,
(255, 255, 100))
screen.blit(score_surf, (10, 10))
lives_surf = hud_font.render(f"Lives: {lives}", True,
(255, 100, 100))
screen.blit(lives_surf,
(SCREEN_WIDTH // 2 - lives_surf.get_width() // 2, 10))
level_surf = hud_font.render(f"Level: {level}", True,
(100, 200, 255))
screen.blit(level_surf,
(SCREEN_WIDTH - level_surf.get_width() - 10, 10))
3. Row-based scoring
Bricks at the top are worth more points than those at the bottom. This is fairer, since they are harder to reach.
def score_for_row(row):
"""Higher rows (closer to top) give more points."""
return (BRICK_ROWS - row) * 10
Row 0 (top) is worth 60 points, row 5 (bottom) is worth 10 points.
4. Lives system
The player starts with 3 lives. When the ball falls off, they lose one. At zero lives, it is Game Over.
# Ball falls off bottom: lose a life
if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
lives -= 1
if lives <= 0:
game_state = STATE_GAME_OVER
else:
reset_ball()
5. End screens with overlay
For the GAME OVER and YOU WIN screens, we first draw a semi-transparent black surface over the entire game, then display the text in the center.
if game_state == STATE_GAME_OVER:
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT),
pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
go_text = big_font.render("GAME OVER", True, (255, 60, 60))
screen.blit(go_text,
(SCREEN_WIDTH // 2 - go_text.get_width() // 2,
SCREEN_HEIGHT // 2 - 40))
Key takeaways
Tip: do not create fonts every frame! SysFont() is slow. Create your fonts once before the loop and reuse them.
Common pitfall: font.render() does not handle line breaks. For multi-line text, you must render each line separately and position them manually.
Pygame functions used:
pygame.font.SysFont(name, size): loads a system fontfont.render(text, antialias, color): creates a text Surfacesurface.get_width(): pixel width (useful for centering)surface.get_height(): pixel height
Complete code
"""Brick Breaker - Chapter 8: Text and Fonts"""
import pygame
import sys
import math
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_BASE_SPEED = 300
BALL_SPEED_INCREMENT = 10
BRICK_ROWS = 6
BRICK_COLS = 10
BRICK_WIDTH = 70
BRICK_HEIGHT = 20
BRICK_PADDING = 5
BRICK_OFFSET_X = 30
BRICK_OFFSET_Y = 50
MAX_LIVES = 3
lives = MAX_LIVES
score = 0
level = 1
# Game states
STATE_PLAYING = 0
STATE_GAME_OVER = 1
STATE_YOU_WIN = 2
game_state = STATE_PLAYING
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)
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))
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()
def create_ball_image(radius):
size = radius * 4
surf = pygame.Surface((size, size), pygame.SRCALPHA)
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)
pygame.draw.circle(surf, (255, 255, 255),
(size // 2, size // 2), radius)
pygame.draw.circle(surf, (255, 255, 255, 200),
(size // 2 - 2, size // 2 - 2), radius // 3)
return surf.convert_alpha()
def create_brick_image(width, height, color):
surf = pygame.Surface((width, height), pygame.SRCALPHA)
pygame.draw.rect(surf, color, (0, 0, width, height))
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 = 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()
def create_bricks():
bricks = []
for row in range(BRICK_ROWS):
color = rainbow_color(row, BRICK_ROWS)
image = create_brick_image(BRICK_WIDTH, BRICK_HEIGHT, color)
for col in range(BRICK_COLS):
x = BRICK_OFFSET_X + col * (BRICK_WIDTH + BRICK_PADDING)
y = BRICK_OFFSET_Y + row * (BRICK_HEIGHT + BRICK_PADDING)
brick = {
"rect": pygame.Rect(x, y, BRICK_WIDTH, BRICK_HEIGHT),
"color": color,
"image": image,
"row": row,
}
bricks.append(brick)
return bricks
def reset_ball():
global ball_x, ball_y, ball_vx, ball_vy, ball_speed
ball_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
def score_for_row(row):
"""Higher rows (closer to top) give more points."""
return (BRICK_ROWS - row) * 10
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 8")
clock = pygame.time.Clock()
# Fonts
hud_font = pygame.font.SysFont("monospace", 18)
big_font = pygame.font.SysFont("monospace", 48, bold=True)
medium_font = pygame.font.SysFont("monospace", 24)
small_font = pygame.font.SysFont("monospace", 14)
paddle_img = create_paddle_image(PADDLE_WIDTH, PADDLE_HEIGHT)
ball_img = create_ball_image(BALL_RADIUS)
bricks = create_bricks()
ball_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
use_mouse = False
running = True
while running:
dt = clock.tick(FPS) / 1000.0
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 event.key == pygame.K_r and game_state != STATE_PLAYING:
lives = MAX_LIVES
score = 0
level = 1
bricks = create_bricks()
reset_ball()
game_state = STATE_PLAYING
if game_state == STATE_PLAYING:
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))
# Move ball
ball_x += ball_vx * dt
ball_y += ball_vy * dt
# Bounce off left wall
if ball_x - BALL_RADIUS <= 0:
ball_x = BALL_RADIUS
ball_vx = abs(ball_vx)
ball_speed += BALL_SPEED_INCREMENT
# Bounce off right wall
if ball_x + BALL_RADIUS >= SCREEN_WIDTH:
ball_x = SCREEN_WIDTH - BALL_RADIUS
ball_vx = -abs(ball_vx)
ball_speed += BALL_SPEED_INCREMENT
# Bounce off top wall
if ball_y - BALL_RADIUS <= 0:
ball_y = BALL_RADIUS
ball_vy = abs(ball_vy)
ball_speed += BALL_SPEED_INCREMENT
# Ball falls off bottom: lose a life
if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
lives -= 1
if lives <= 0:
game_state = STATE_GAME_OVER
else:
reset_ball()
# Ball vs paddle collision
paddle_rect = pygame.Rect(paddle_x, paddle_y,
PADDLE_WIDTH, PADDLE_HEIGHT)
ball_rect = pygame.Rect(ball_x - BALL_RADIUS,
ball_y - BALL_RADIUS,
BALL_RADIUS * 2, BALL_RADIUS * 2)
if ball_rect.colliderect(paddle_rect) and ball_vy > 0:
ball_y = paddle_y - BALL_RADIUS
hit_pos = (ball_x - paddle_x) / PADDLE_WIDTH
angle = 150 - hit_pos * 120
rad = math.radians(angle)
ball_speed += BALL_SPEED_INCREMENT
ball_vx = ball_speed * math.cos(rad)
ball_vy = -abs(ball_speed * math.sin(rad))
# Ball vs bricks collision
for brick in bricks[:]:
if ball_rect.colliderect(brick["rect"]):
score += score_for_row(brick["row"])
bricks.remove(brick)
br = brick["rect"]
if ball_x >= br.left and ball_x <= br.right:
ball_vy = -ball_vy
else:
ball_vx = -ball_vx
ball_speed += BALL_SPEED_INCREMENT
break
# Check if all bricks destroyed
if len(bricks) == 0:
game_state = STATE_YOU_WIN
# Draw
screen.fill(BG_COLOR)
for brick in bricks:
screen.blit(brick["image"], brick["rect"])
screen.blit(paddle_img, (paddle_x, paddle_y))
ball_offset = BALL_RADIUS * 2
screen.blit(ball_img, (ball_x - ball_offset, ball_y - ball_offset))
# HUD
score_surf = hud_font.render(f"Score: {score}", True, (255, 255, 100))
screen.blit(score_surf, (10, 10))
lives_surf = hud_font.render(f"Lives: {lives}", True, (255, 100, 100))
screen.blit(lives_surf,
(SCREEN_WIDTH // 2 - lives_surf.get_width() // 2, 10))
level_surf = hud_font.render(f"Level: {level}", True, (100, 200, 255))
screen.blit(level_surf,
(SCREEN_WIDTH - level_surf.get_width() - 10, 10))
# Game state overlays
if game_state == STATE_GAME_OVER:
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT),
pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
go_text = big_font.render("GAME OVER", True, (255, 60, 60))
screen.blit(go_text,
(SCREEN_WIDTH // 2 - go_text.get_width() // 2,
SCREEN_HEIGHT // 2 - 40))
final_score = medium_font.render(f"Final score: {score}",
True, (255, 255, 255))
screen.blit(final_score,
(SCREEN_WIDTH // 2 - final_score.get_width() // 2,
SCREEN_HEIGHT // 2 + 20))
restart = medium_font.render("Press R to restart",
True, (200, 200, 200))
screen.blit(restart,
(SCREEN_WIDTH // 2 - restart.get_width() // 2,
SCREEN_HEIGHT // 2 + 60))
elif game_state == STATE_YOU_WIN:
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT),
pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
win_text = big_font.render("YOU WIN!", True, (100, 255, 100))
screen.blit(win_text,
(SCREEN_WIDTH // 2 - win_text.get_width() // 2,
SCREEN_HEIGHT // 2 - 40))
final_score = medium_font.render(f"Final score: {score}",
True, (255, 255, 255))
screen.blit(final_score,
(SCREEN_WIDTH // 2 - final_score.get_width() // 2,
SCREEN_HEIGHT // 2 + 20))
restart = medium_font.render("Press R to restart",
True, (200, 200, 200))
screen.blit(restart,
(SCREEN_WIDTH // 2 - restart.get_width() // 2,
SCREEN_HEIGHT // 2 + 60))
info = small_font.render(
"[M] Mouse/Keyboard [Arrows] Move [R] Restart [ESC] Quit",
True, (150, 150, 150))
screen.blit(info, (10, SCREEN_HEIGHT - 25))
pygame.display.flip()
pygame.quit()
sys.exit()
Download
Download the source code for this chapter: brick-breaker-ch08.zip
