What you will learn
This chapter introduces a particle system. Particles are small graphical elements with a position, velocity, lifetime, color and size. We use them for three effects: brick explosion, sparks on the paddle, and dust on wall bounces.
The concept
A particle system has two parts:
- The Particle class: a small object with position (x, y), velocity (vx, vy), lifetime, color and size. Each frame, the particle moves, is affected by gravity, and ages. When its lifetime reaches zero, it dies.
- The ParticleSystem class: a manager that holds all particles. It updates them, removes dead ones, and draws them. It also provides emission methods for each type of effect.
The particle draws itself as a circle whose opacity decreases over time and whose size shrinks progressively.
Step by step
1. The Particle class
Each particle has its own simple physics: linear movement plus gravity.
class Particle:
def __init__(self, x, y, vx, vy, lifetime, color, size):
self.x = float(x)
self.y = float(y)
self.vx = vx
self.vy = vy
self.lifetime = lifetime
self.max_lifetime = lifetime
self.color = color
self.size = size
self.alive = True
self.gravity = 120.0
def update(self, dt):
self.x += self.vx * dt
self.y += self.vy * dt
self.vy += self.gravity * dt
self.lifetime -= dt
if self.lifetime <= 0:
self.alive = False
2. The ParticleSystem class
The manager handles the particle collection and provides specific emission methods.
class ParticleSystem:
def __init__(self):
self.particles = []
def update(self, dt):
for p in self.particles:
p.update(dt)
self.particles = [
p for p in self.particles if p.alive]
def draw(self, surface):
for p in self.particles:
p.draw(surface)
3. Brick explosion
When a brick is destroyed, we emit 15 particles in all directions from its center. The colors vary slightly around the brick color.
def emit_brick_explosion(self, rect, color):
cx = rect.centerx
cy = rect.centery
for _ in range(15):
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(60, 200)
vx = speed * math.cos(angle)
vy = speed * math.sin(angle) - 50
lifetime = random.uniform(0.3, 0.8)
size = random.randint(2, 5)
self.particles.append(
Particle(cx, cy, vx, vy, lifetime, color, size))
4. Paddle sparks
At the point where the ball hits the paddle, we emit white-yellow sparks upward.
5. Wall bounce dust
When the ball bounces off a wall, small gray particles are emitted in the direction away from the wall.
Key takeaways
Tip: limit the particle count by keeping short lifetimes. 15 particles per brick with 0.5s lifetime gives a nice effect without slowing the game.
Common pitfall: forgetting to remove dead particles. Without filtering in update(), the list grows indefinitely and the game slows down.
Gravity: even light gravity (120 pixels/s2) makes particles look much more natural. Without it, debris seems to float in space.
Complete code
"""Brick Breaker - Chapter 15: Particles"""
import pygame
import sys
import math
import array
import random
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60
BG_COLOR = (20, 20, 40)
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_SPEED = 8
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
# Game states
STATE_MENU = 0
STATE_PLAYING = 1
STATE_PAUSED = 2
STATE_GAME_OVER = 3
STATE_YOU_WIN = 4
# Animation constants
TRAIL_LENGTH = 8
TRAIL_INTERVAL = 0.016
BRICK_SHATTER_DURATION = 0.3
PADDLE_GLOW_DURATION = 0.2
# ── Sound generation ────────────────────────────────────────────────
def generate_beep(frequency=440, duration_ms=100, volume=0.3):
sample_rate = 44100
n_samples = int(sample_rate * duration_ms / 1000)
buf = array.array("h", [0] * n_samples)
max_amp = int(32767 * volume)
for i in range(n_samples):
t = i / sample_rate
value = int(max_amp * math.sin(2 * math.pi * frequency * t))
fade_samples = min(500, n_samples // 4)
if i >= n_samples - fade_samples:
value = int(value * (n_samples - i) / fade_samples)
buf[i] = value
return pygame.mixer.Sound(buffer=buf)
snd_paddle = generate_beep(frequency=500, duration_ms=60, volume=0.3)
snd_wall = generate_beep(frequency=300, duration_ms=50, volume=0.2)
snd_brick = generate_beep(frequency=700, duration_ms=80, volume=0.3)
snd_lose_life = generate_beep(frequency=150, duration_ms=300, volume=0.4)
snd_game_over = generate_beep(frequency=100, duration_ms=500, volume=0.5)
snd_win = generate_beep(frequency=880, duration_ms=400, volume=0.4)
# ── Helper functions ────────────────────────────────────────────────
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)
# ── Particle System ─────────────────────────────────────────────────
class Particle:
"""A single particle with position, velocity, lifetime, color, size."""
def __init__(self, x, y, vx, vy, lifetime, color, size):
self.x = float(x)
self.y = float(y)
self.vx = vx
self.vy = vy
self.lifetime = lifetime
self.max_lifetime = lifetime
self.color = color
self.size = size
self.alive = True
# Gravity: particles fall down slightly
self.gravity = 120.0
def update(self, dt):
self.x += self.vx * dt
self.y += self.vy * dt
self.vy += self.gravity * dt
self.lifetime -= dt
if self.lifetime <= 0:
self.alive = False
def draw(self, surface):
if not self.alive:
return
progress = 1.0 - (self.lifetime / self.max_lifetime)
alpha = int(255 * (1 - progress))
current_size = max(1, int(self.size * (1 - progress * 0.5)))
if alpha <= 0:
return
r, g, b = self.color
surf = pygame.Surface((current_size * 2, current_size * 2), pygame.SRCALPHA)
pygame.draw.circle(surf, (r, g, b, alpha),
(current_size, current_size), current_size)
surface.blit(surf, (int(self.x) - current_size, int(self.y) - current_size))
class ParticleSystem:
"""Manages all particles in the game."""
def __init__(self):
self.particles = []
def update(self, dt):
for p in self.particles:
p.update(dt)
self.particles = [p for p in self.particles if p.alive]
def draw(self, surface):
for p in self.particles:
p.draw(surface)
def clear(self):
self.particles.clear()
def emit_brick_explosion(self, rect, color):
"""Emit colored particles bursting outward from a destroyed brick."""
cx = rect.centerx
cy = rect.centery
for _ in range(15):
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(60, 200)
vx = speed * math.cos(angle)
vy = speed * math.sin(angle) - 50 # bias upward
lifetime = random.uniform(0.3, 0.8)
# Vary the brick color slightly
r = min(255, max(0, color[0] + random.randint(-30, 30)))
g = min(255, max(0, color[1] + random.randint(-30, 30)))
b = min(255, max(0, color[2] + random.randint(-30, 30)))
size = random.randint(2, 5)
self.particles.append(
Particle(cx, cy, vx, vy, lifetime, (r, g, b), size))
def emit_paddle_sparks(self, paddle_rect, ball_x):
"""Emit bright sparks at the point where the ball hits the paddle."""
cx = ball_x
cy = paddle_rect.top
for _ in range(10):
angle = random.uniform(-math.pi, 0) # upward half
speed = random.uniform(40, 150)
vx = speed * math.cos(angle)
vy = speed * math.sin(angle)
lifetime = random.uniform(0.2, 0.5)
# Bright white-yellow sparks
r = random.randint(220, 255)
g = random.randint(200, 255)
b = random.randint(100, 200)
size = random.randint(1, 3)
self.particles.append(
Particle(cx, cy, vx, vy, lifetime, (r, g, b), size))
def emit_wall_dust(self, x, y, direction):
"""Emit small dust particles when ball bounces off a wall.
direction: 'left', 'right', or 'top'
"""
for _ in range(5):
if direction == "left":
vx = random.uniform(20, 80)
vy = random.uniform(-50, 50)
elif direction == "right":
vx = random.uniform(-80, -20)
vy = random.uniform(-50, 50)
else: # top
vx = random.uniform(-50, 50)
vy = random.uniform(20, 80)
lifetime = random.uniform(0.2, 0.4)
gray = random.randint(150, 220)
size = random.randint(1, 3)
self.particles.append(
Particle(x, y, vx, vy, lifetime, (gray, gray, gray), size))
# ── Animation classes ───────────────────────────────────────────────
class BrickShatter:
"""Brick shatter animation: flash white, then fade out."""
def __init__(self, rect, color):
self.rect = pygame.Rect(rect)
self.color = color
self.timer = 0.0
self.duration = BRICK_SHATTER_DURATION
self.alive = True
def update(self, dt):
self.timer += dt
if self.timer >= self.duration:
self.alive = False
def draw(self, surface):
progress = self.timer / self.duration
if progress < 0.3:
flash = int(255 * (1 - progress / 0.3))
r = min(255, self.color[0] + flash)
g = min(255, self.color[1] + flash)
b = min(255, self.color[2] + flash)
alpha = 255
else:
fade = (progress - 0.3) / 0.7
r, g, b = self.color
alpha = int(255 * (1 - fade))
if alpha <= 0:
return
shrink = int(progress * 10)
draw_rect = self.rect.inflate(-shrink * 2, -shrink * 2)
if draw_rect.width <= 0 or draw_rect.height <= 0:
return
surf = pygame.Surface((draw_rect.width, draw_rect.height), pygame.SRCALPHA)
surf.fill((r, g, b, alpha))
surface.blit(surf, draw_rect.topleft)
class BallTrail:
"""Trail effect behind the ball."""
def __init__(self):
self.positions = []
self.timer = 0.0
def update(self, dt, ball_x, ball_y):
self.timer += dt
if self.timer >= TRAIL_INTERVAL:
self.timer -= TRAIL_INTERVAL
self.positions.append((ball_x, ball_y))
if len(self.positions) > TRAIL_LENGTH:
self.positions.pop(0)
def draw(self, surface):
count = len(self.positions)
for i, (x, y) in enumerate(self.positions):
ratio = (i + 1) / count if count > 0 else 0
alpha = int(80 * ratio)
radius = max(1, int(BALL_RADIUS * ratio * 0.8))
trail_surf = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
pygame.draw.circle(trail_surf, (100, 100, 255, alpha),
(radius, radius), radius)
surface.blit(trail_surf, (int(x) - radius, int(y) - radius))
def reset(self):
self.positions.clear()
self.timer = 0.0
class PaddleGlow:
"""Glow effect on the paddle when the ball bounces on it."""
def __init__(self):
self.timer = 0.0
self.active = False
self.intensity = 0.0
def trigger(self):
self.active = True
self.timer = 0.0
self.intensity = 1.0
def update(self, dt):
if self.active:
self.timer += dt
self.intensity = max(0.0, 1.0 - self.timer / PADDLE_GLOW_DURATION)
if self.timer >= PADDLE_GLOW_DURATION:
self.active = False
def draw(self, surface, paddle_rect):
if not self.active or self.intensity <= 0:
return
glow_alpha = int(150 * self.intensity)
glow_expand = int(6 * self.intensity)
glow_rect = paddle_rect.inflate(glow_expand * 2, glow_expand * 2)
glow_surf = pygame.Surface(
(glow_rect.width, glow_rect.height), pygame.SRCALPHA)
glow_surf.fill((200, 200, 255, glow_alpha))
surface.blit(glow_surf, glow_rect.topleft)
# ── Sprite classes ──────────────────────────────────────────────────
class Paddle(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((PADDLE_WIDTH, PADDLE_HEIGHT), pygame.SRCALPHA)
for y in range(PADDLE_HEIGHT):
brightness = 200 + int(55 * (1 - y / PADDLE_HEIGHT))
pygame.draw.line(self.image, (brightness, brightness, brightness),
(0, y), (PADDLE_WIDTH, y))
pygame.draw.rect(self.image, (0, 0, 0, 0), (0, 0, 3, 3))
pygame.draw.rect(self.image, (0, 0, 0, 0), (PADDLE_WIDTH - 3, 0, 3, 3))
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect()
self.rect.centerx = SCREEN_WIDTH // 2
self.rect.y = SCREEN_HEIGHT - 40
self.use_mouse = False
def update(self):
if self.use_mouse:
mx, _ = pygame.mouse.get_pos()
self.rect.centerx = mx
else:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.rect.x -= PADDLE_SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.rect.x += PADDLE_SPEED
self.rect.left = max(0, self.rect.left)
self.rect.right = min(SCREEN_WIDTH, self.rect.right)
class Ball(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
size = BALL_RADIUS * 4
self.image = pygame.Surface((size, size), pygame.SRCALPHA)
center = size // 2
for r in range(BALL_RADIUS * 2, BALL_RADIUS, -1):
alpha = int(100 * (1 - (r - BALL_RADIUS) / BALL_RADIUS))
pygame.draw.circle(self.image, (100, 100, 255, alpha), (center, center), r)
pygame.draw.circle(self.image, (255, 255, 255), (center, center), BALL_RADIUS)
pygame.draw.circle(self.image, (255, 255, 255, 200),
(center - 2, center - 2), BALL_RADIUS // 3)
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect()
self.float_x = float(SCREEN_WIDTH // 2)
self.float_y = float(SCREEN_HEIGHT // 2)
self.rect.center = (int(self.float_x), int(self.float_y))
self.speed = BALL_BASE_SPEED
angle = math.radians(-60)
self.vx = self.speed * math.cos(angle)
self.vy = self.speed * math.sin(angle)
def reset(self):
self.float_x = float(SCREEN_WIDTH // 2)
self.float_y = float(SCREEN_HEIGHT // 2)
self.rect.center = (int(self.float_x), int(self.float_y))
self.speed = BALL_BASE_SPEED
angle = math.radians(-60)
self.vx = self.speed * math.cos(angle)
self.vy = self.speed * math.sin(angle)
def update(self, dt):
self.float_x += self.vx * dt
self.float_y += self.vy * dt
self.rect.center = (int(self.float_x), int(self.float_y))
def bounce_wall(self):
bounced = False
wall_direction = None
if self.float_x - BALL_RADIUS <= 0:
self.float_x = BALL_RADIUS
self.vx = abs(self.vx)
self.speed += BALL_SPEED_INCREMENT
bounced = True
wall_direction = "left"
if self.float_x + BALL_RADIUS >= SCREEN_WIDTH:
self.float_x = SCREEN_WIDTH - BALL_RADIUS
self.vx = -abs(self.vx)
self.speed += BALL_SPEED_INCREMENT
bounced = True
wall_direction = "right"
if self.float_y - BALL_RADIUS <= 0:
self.float_y = BALL_RADIUS
self.vy = abs(self.vy)
self.speed += BALL_SPEED_INCREMENT
bounced = True
wall_direction = "top"
if bounced:
self.rect.center = (int(self.float_x), int(self.float_y))
return bounced, wall_direction
def fell_off_bottom(self):
return self.float_y - BALL_RADIUS > SCREEN_HEIGHT
def bounce_off_paddle(self, paddle):
if self.rect.colliderect(paddle.rect) and self.vy > 0:
self.float_y = paddle.rect.top - BALL_RADIUS
hit_pos = (self.float_x - paddle.rect.left) / PADDLE_WIDTH
angle = 150 - hit_pos * 120
rad = math.radians(angle)
self.speed += BALL_SPEED_INCREMENT
self.vx = self.speed * math.cos(rad)
self.vy = -abs(self.speed * math.sin(rad))
self.rect.center = (int(self.float_x), int(self.float_y))
return True
return False
class Brick(pygame.sprite.Sprite):
def __init__(self, x, y, color, row):
super().__init__()
self.image = pygame.Surface((BRICK_WIDTH, BRICK_HEIGHT), pygame.SRCALPHA)
pygame.draw.rect(self.image, color, (0, 0, BRICK_WIDTH, BRICK_HEIGHT))
highlight = tuple(min(255, c + 50) for c in color)
pygame.draw.line(self.image, highlight, (1, 1), (BRICK_WIDTH - 2, 1))
pygame.draw.line(self.image, highlight, (1, 1), (1, BRICK_HEIGHT - 2))
shadow = tuple(max(0, c - 50) for c in color)
pygame.draw.line(self.image, shadow, (1, BRICK_HEIGHT - 1),
(BRICK_WIDTH - 1, BRICK_HEIGHT - 1))
pygame.draw.line(self.image, shadow, (BRICK_WIDTH - 1, 1),
(BRICK_WIDTH - 1, BRICK_HEIGHT - 1))
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect(topleft=(x, y))
self.row = row
self.color = color
def score_value(self):
return (BRICK_ROWS - self.row) * 10
def create_brick_group():
group = pygame.sprite.Group()
for row in range(BRICK_ROWS):
color = rainbow_color(row, 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)
group.add(Brick(x, y, color, row))
return group
# ── Game setup ──────────────────────────────────────────────────────
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 15")
clock = pygame.time.Clock()
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 = Paddle()
ball = Ball()
bricks = create_brick_group()
paddle_group = pygame.sprite.GroupSingle(paddle)
ball_group = pygame.sprite.GroupSingle(ball)
# Animation and particle objects
ball_trail = BallTrail()
paddle_glow = PaddleGlow()
shatter_animations = []
particles = ParticleSystem()
lives = MAX_LIVES
score = 0
level = 1
game_state = STATE_MENU
master_volume = 0.5
sound_muted = False
menu_blink_timer = 0.0
def play_sound(sound):
if not sound_muted:
sound.set_volume(master_volume)
sound.play()
def start_game():
global lives, score, level, bricks, game_state, shatter_animations
lives = MAX_LIVES
score = 0
level = 1
bricks = create_brick_group()
ball.reset()
ball_trail.reset()
shatter_animations = []
particles.clear()
game_state = STATE_PLAYING
# ── Draw helpers ────────────────────────────────────────────────────
def draw_menu():
global menu_blink_timer
menu_blink_timer += clock.get_time() / 1000.0
screen.fill(BG_COLOR)
title = big_font.render("BRICK BREAKER", True, (255, 200, 50))
screen.blit(title, (SCREEN_WIDTH // 2 - title.get_width() // 2,
SCREEN_HEIGHT // 3 - 30))
subtitle = medium_font.render("Particles Edition", True, (200, 200, 255))
screen.blit(subtitle, (SCREEN_WIDTH // 2 - subtitle.get_width() // 2,
SCREEN_HEIGHT // 3 + 30))
if int(menu_blink_timer * 2) % 2 == 0:
start_text = medium_font.render("Press SPACE to start", True, (255, 255, 255))
screen.blit(start_text, (SCREEN_WIDTH // 2 - start_text.get_width() // 2,
SCREEN_HEIGHT // 2 + 20))
def draw_hud():
score_surf = hud_font.render("Score: {}".format(score), True, (255, 255, 100))
screen.blit(score_surf, (10, 10))
lives_surf = hud_font.render("Lives: {}".format(lives), True, (255, 100, 100))
screen.blit(lives_surf, (SCREEN_WIDTH // 2 - lives_surf.get_width() // 2, 10))
level_surf = hud_font.render("Level: {}".format(level), True, (100, 200, 255))
screen.blit(level_surf, (SCREEN_WIDTH - level_surf.get_width() - 10, 10))
vol_label = "MUTED" if sound_muted else "Vol: {}".format(int(master_volume * 100))
vol_color = (255, 80, 80) if sound_muted else (150, 150, 150)
vol_surf = small_font.render(vol_label, True, vol_color)
screen.blit(vol_surf, (SCREEN_WIDTH - vol_surf.get_width() - 10, 30))
count_surf = small_font.render(
"Particles: {}".format(len(particles.particles)),
True, (100, 100, 100))
screen.blit(count_surf, (10, 30))
def draw_overlay(title_text, title_color):
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
t = big_font.render(title_text, True, title_color)
screen.blit(t, (SCREEN_WIDTH // 2 - t.get_width() // 2, SCREEN_HEIGHT // 2 - 40))
fs = medium_font.render("Final score: {}".format(score), True, (255, 255, 255))
screen.blit(fs, (SCREEN_WIDTH // 2 - fs.get_width() // 2, SCREEN_HEIGHT // 2 + 20))
r = medium_font.render("Press SPACE for menu", True, (200, 200, 200))
screen.blit(r, (SCREEN_WIDTH // 2 - r.get_width() // 2, SCREEN_HEIGHT // 2 + 60))
# ── Main loop ───────────────────────────────────────────────────────
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_F1:
sound_muted = not sound_muted
if event.key in (pygame.K_PLUS, pygame.K_KP_PLUS, pygame.K_EQUALS):
master_volume = min(1.0, master_volume + 0.1)
if event.key in (pygame.K_MINUS, pygame.K_KP_MINUS):
master_volume = max(0.0, master_volume - 0.1)
if game_state == STATE_MENU:
if event.key == pygame.K_SPACE:
start_game()
elif game_state == STATE_PLAYING:
if event.key == pygame.K_m:
paddle.use_mouse = not paddle.use_mouse
if event.key == pygame.K_p:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
if event.key == pygame.K_p:
game_state = STATE_PLAYING
elif game_state in (STATE_GAME_OVER, STATE_YOU_WIN):
if event.key == pygame.K_SPACE:
game_state = STATE_MENU
# ── Update ──────────────────────────────────────────────────
if game_state == STATE_PLAYING:
paddle_group.update()
ball.update(dt)
# Update animations and particles
ball_trail.update(dt, ball.float_x, ball.float_y)
paddle_glow.update(dt)
particles.update(dt)
for anim in shatter_animations:
anim.update(dt)
shatter_animations = [a for a in shatter_animations if a.alive]
# Wall bounces with dust particles
bounced, wall_dir = ball.bounce_wall()
if bounced:
play_sound(snd_wall)
if wall_dir == "left":
particles.emit_wall_dust(
BALL_RADIUS, ball.float_y, "left")
elif wall_dir == "right":
particles.emit_wall_dust(
SCREEN_WIDTH - BALL_RADIUS, ball.float_y, "right")
elif wall_dir == "top":
particles.emit_wall_dust(
ball.float_x, BALL_RADIUS, "top")
if ball.fell_off_bottom():
lives -= 1
if lives <= 0:
game_state = STATE_GAME_OVER
play_sound(snd_game_over)
else:
play_sound(snd_lose_life)
ball.reset()
ball_trail.reset()
if ball.bounce_off_paddle(paddle):
play_sound(snd_paddle)
paddle_glow.trigger()
# Paddle sparks
particles.emit_paddle_sparks(paddle.rect, ball.float_x)
hit_bricks = pygame.sprite.spritecollide(ball, bricks, True)
if hit_bricks:
for brick in hit_bricks:
score += brick.score_value()
# Shatter animation
shatter_animations.append(
BrickShatter(brick.rect, brick.color))
# Particle explosion
particles.emit_brick_explosion(brick.rect, brick.color)
ball.vy = -ball.vy
ball.speed += BALL_SPEED_INCREMENT
play_sound(snd_brick)
if len(bricks) == 0:
game_state = STATE_YOU_WIN
play_sound(snd_win)
# ── Draw ────────────────────────────────────────────────────
if game_state == STATE_MENU:
draw_menu()
elif game_state == STATE_PLAYING:
screen.fill(BG_COLOR)
bricks.draw(screen)
for anim in shatter_animations:
anim.draw(screen)
ball_trail.draw(screen)
paddle_glow.draw(screen, paddle.rect)
paddle_group.draw(screen)
ball_group.draw(screen)
particles.draw(screen)
draw_hud()
info = small_font.render(
"[M] Mouse [P] Pause [F1] Mute [+/-] Volume [ESC] Quit",
True, (150, 150, 150))
screen.blit(info, (10, SCREEN_HEIGHT - 25))
elif game_state == STATE_PAUSED:
screen.fill(BG_COLOR)
bricks.draw(screen)
for anim in shatter_animations:
anim.draw(screen)
ball_trail.draw(screen)
paddle_glow.draw(screen, paddle.rect)
paddle_group.draw(screen)
ball_group.draw(screen)
particles.draw(screen)
draw_hud()
draw_overlay("PAUSED", (200, 200, 255))
elif game_state == STATE_GAME_OVER:
screen.fill(BG_COLOR)
bricks.draw(screen)
for anim in shatter_animations:
anim.draw(screen)
particles.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_overlay("GAME OVER", (255, 60, 60))
elif game_state == STATE_YOU_WIN:
screen.fill(BG_COLOR)
for anim in shatter_animations:
anim.draw(screen)
particles.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_overlay("YOU WIN!", (100, 255, 100))
pygame.display.flip()
pygame.quit()
sys.exit()
Download
Download the source code for this chapter: brick-breaker-ch15.zip
