Ce que vous allez apprendre
Dans ce chapitre, nous explorons les techniques d'optimisation avec Pygame : compteur FPS, suivi des appels de dessin, mode dirty rect (ne redessiner que les zones modifiees), et utilisation de convert() et convert_alpha() pour accelerer le blit. Le test utilise plus de 500 briques.
Le concept
La performance dans un jeu Pygame depend de trois facteurs principaux :
- Le nombre d'appels de dessin : chaque
surface.blit()a un cout. Moins on en fait, mieux c'est. - Le format des surfaces :
convert()etconvert_alpha()adaptent le format pixel de la surface a celui de l'ecran, accelerant le blit de 3 a 10 fois. - La zone redessinee : avec le dirty rect rendering, on ne met a jour que les zones qui ont change, ce qui reduit considerablement le travail.
Etape par etape
1. Le moniteur de performances
La classe PerfMonitor mesure le temps de chaque frame, calcule le FPS moyen, et compte les appels de dessin. Les resultats s'affichent en surimpression.
class PerfMonitor:
def begin_frame(self):
self.frame_draw_calls = 0
self._frame_start = time.perf_counter()
def count_draw(self, n=1):
self.frame_draw_calls += n
def end_frame(self, dt):
elapsed = time.perf_counter()
- self._frame_start
# ... calcul FPS ...
2. convert() et convert_alpha()
Chaque surface de sprite est convertie avec convert_alpha() apres sa creation. Cette operation aligne le format interne des pixels sur celui de l'ecran, ce qui elimine les conversions couteuses lors du blit.
# AVANT : surface brute (lent)
self.image = pygame.Surface((w, h), pygame.SRCALPHA)
# ... dessiner ...
# APRES : surface convertie (rapide)
self.image = self.image.convert_alpha()
3. Dirty rect rendering
Le mode dirty rect (touche D) ne redessine que les zones modifiees. L'arriere-plan statique (fond + briques intactes) est capture une fois. A chaque frame, seules les zones qui ont bouge sont restaurees et redessinee.
class DirtyRectRenderer:
def mark_dirty(self, rect):
self.dirty_rects.append(rect)
def restore_dirty(self, surface):
for rect in self.dirty_rects:
surface.blit(self.bg_buffer,
rect.topleft, rect)
def flip(self, screen):
pygame.display.update(self.dirty_rects)
4. Slots pour les particules
La classe Particle utilise __slots__ pour reduire la consommation memoire et accelerer l'acces aux attributs quand il y a beaucoup de particules.
Conseils de profilage
Pour profiler votre jeu Pygame :
- Utilisez
time.perf_counter()pour mesurer des sections de code precises. - Lancez
python -m cProfile main.pypour un profil global. - Surveillez le nombre de
blitpar frame : c'est souvent le goulot d'etranglement. - Preferez
pygame.display.update(rects)aflip()quand peu de choses changent.
Points cles a retenir
Astuce : appelez toujours convert_alpha() sur vos surfaces apres creation. C'est l'optimisation la plus simple et la plus efficace.
Piege courant : le dirty rect rendering ajoute de la complexite. Il n'est utile que quand une grande partie de l'ecran est statique.
Code complet
"""Brick Breaker - Chapter 21: Optimization"""
import pygame
import sys
import math
import array
import random
import json
import os
import time
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
GAME_WIDTH = 800
GAME_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
# Stress-test: many bricks to showcase optimization
BRICK_ROWS = 20
BRICK_COLS = 25
BRICK_WIDTH = 28
BRICK_HEIGHT = 10
BRICK_PADDING = 2
BRICK_OFFSET_X = 15
BRICK_OFFSET_Y = 40
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
HIGH_SCORE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"highscores.json")
# -- 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)
# -- Helpers ----------------------------------------------------------------
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 load_high_scores():
if os.path.exists(HIGH_SCORE_FILE):
try:
with open(HIGH_SCORE_FILE, "r") as f:
return json.load(f)[:10]
except (json.JSONDecodeError, IOError):
pass
return []
def save_high_scores(scores):
scores = sorted(scores, key=lambda s: s["score"], reverse=True)[:10]
try:
with open(HIGH_SCORE_FILE, "w") as f:
json.dump(scores, f, indent=2)
except IOError:
pass
def add_high_score(new_score):
scores = load_high_scores()
scores.append({"score": new_score})
scores = sorted(scores, key=lambda s: s["score"], reverse=True)[:10]
save_high_scores(scores)
# -- Performance monitor ---------------------------------------------------
class PerfMonitor:
"""Tracks FPS, draw calls, frame times, and provides profiling info."""
def __init__(self):
self.fps = 0.0
self.draw_calls = 0
self.frame_draw_calls = 0
self.frame_times = []
self.update_timer = 0.0
self.avg_frame_time = 0.0
self.min_frame_time = 0.0
self.max_frame_time = 0.0
self.total_bricks = 0
self.visible = True
def begin_frame(self):
self.frame_draw_calls = 0
self._frame_start = time.perf_counter()
def count_draw(self, n=1):
self.frame_draw_calls += n
def end_frame(self, dt):
elapsed = time.perf_counter() - self._frame_start
self.frame_times.append(elapsed)
if len(self.frame_times) > 60:
self.frame_times.pop(0)
self.draw_calls = self.frame_draw_calls
self.update_timer += dt
if self.update_timer >= 0.5:
self.update_timer = 0.0
if self.frame_times:
self.avg_frame_time = (sum(self.frame_times)
/ len(self.frame_times))
self.min_frame_time = min(self.frame_times)
self.max_frame_time = max(self.frame_times)
self.fps = 1.0 / self.avg_frame_time if self.avg_frame_time > 0 else 0
def draw(self, surface, font, dirty_mode=False, x=10, y=10):
if not self.visible:
return
lines = [
"FPS: {:.1f}".format(self.fps),
"Frame: {:.2f}ms (avg) / {:.2f}ms (max)".format(
self.avg_frame_time * 1000, self.max_frame_time * 1000),
"Draw calls: {}".format(self.draw_calls),
"Bricks: {}".format(self.total_bricks),
"Render: {}".format("DIRTY RECT" if dirty_mode else "FULL"),
]
# Background box
box_w = 340
box_h = len(lines) * 16 + 8
bg = pygame.Surface((box_w, box_h), pygame.SRCALPHA)
bg.fill((0, 0, 0, 180))
surface.blit(bg, (x - 2, y - 2))
for i, line in enumerate(lines):
color = (0, 255, 0) if self.fps >= 55 else (
(255, 255, 0) if self.fps >= 30 else (255, 50, 50))
ts = font.render(line, True, color)
surface.blit(ts, (x, y + i * 16))
# -- Particle System (optimized) -------------------------------------------
class Particle:
__slots__ = ['x', 'y', 'vx', 'vy', 'lifetime', 'max_lifetime',
'color', 'size', 'alive', 'gravity']
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
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 or current_size <= 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:
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, perf=None):
for p in self.particles:
p.draw(surface)
if perf:
perf.count_draw(len(self.particles))
def clear(self):
self.particles.clear()
def emit_brick_explosion(self, rect, color, count=8):
cx, cy = rect.centerx, rect.centery
for _ in range(count):
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(60, 200)
vx = speed * math.cos(angle)
vy = speed * math.sin(angle) - 50
lt = random.uniform(0.3, 0.8)
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)))
self.particles.append(
Particle(cx, cy, vx, vy, lt, (r, g, b),
random.randint(2, 4)))
def emit_paddle_sparks(self, paddle_rect, ball_x):
cx, cy = ball_x, paddle_rect.top
for _ in range(6):
angle = random.uniform(-math.pi, 0)
speed = random.uniform(40, 150)
self.particles.append(
Particle(cx, cy, speed * math.cos(angle),
speed * math.sin(angle),
random.uniform(0.2, 0.5),
(random.randint(220, 255), random.randint(200, 255),
random.randint(100, 200)),
random.randint(1, 3)))
def emit_wall_dust(self, x, y, direction):
for _ in range(3):
if direction == "left":
vx, vy = random.uniform(20, 80), random.uniform(-50, 50)
elif direction == "right":
vx, vy = random.uniform(-80, -20), random.uniform(-50, 50)
else:
vx, vy = random.uniform(-50, 50), random.uniform(20, 80)
gray = random.randint(150, 220)
self.particles.append(
Particle(x, y, vx, vy, random.uniform(0.2, 0.4),
(gray, gray, gray), random.randint(1, 3)))
# -- Animation classes ------------------------------------------------------
class BrickShatter:
__slots__ = ['rect', 'color', 'timer', 'duration', 'alive']
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:
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, perf=None):
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))
ts = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
pygame.draw.circle(ts, (100, 100, 255, alpha),
(radius, radius), radius)
surface.blit(ts, (int(x) - radius, int(y) - radius))
if perf:
perf.count_draw(count)
def reset(self):
self.positions.clear()
self.timer = 0.0
class PaddleGlow:
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, perf=None):
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)
if perf:
perf.count_draw()
# -- Sprite classes (with convert / convert_alpha optimization) -------------
class Paddle(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
# Use convert_alpha() for faster blitting of per-pixel alpha surfaces
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))
# convert_alpha() makes blitting much faster
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect()
self.rect.centerx = GAME_WIDTH // 2
self.rect.y = GAME_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(GAME_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)
# convert_alpha() optimization
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect()
self.float_x = float(GAME_WIDTH // 2)
self.float_y = float(GAME_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(GAME_WIDTH // 2)
self.float_y = float(GAME_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 >= GAME_WIDTH:
self.float_x = GAME_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 > GAME_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):
"""Brick with pre-rendered convert_alpha() surface for fast blitting."""
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))
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))
# Key optimization: convert_alpha()
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():
"""Create 500+ bricks for performance stress-testing."""
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
# -- Dirty rect renderer ---------------------------------------------------
class DirtyRectRenderer:
"""Tracks changed areas and only redraws those regions.
When enabled, the background is drawn once to a buffer. Each frame,
only the rectangles that changed (ball, paddle, destroyed bricks, etc.)
are restored from the background and redrawn. This can be much faster
when most of the screen is static.
"""
def __init__(self):
self.enabled = False
self.bg_buffer = None
self.dirty_rects = []
self.need_full_redraw = True
def toggle(self):
self.enabled = not self.enabled
self.need_full_redraw = True
def mark_dirty(self, rect):
"""Mark a rectangular area as needing redraw."""
# Expand slightly to catch anti-aliasing edges
expanded = pygame.Rect(rect).inflate(4, 4)
expanded = expanded.clip(pygame.Rect(0, 0, GAME_WIDTH, GAME_HEIGHT))
self.dirty_rects.append(expanded)
def capture_background(self, surface):
"""Take a snapshot of the current static background."""
self.bg_buffer = surface.copy()
def restore_dirty(self, surface):
"""Restore background under dirty rects."""
if self.bg_buffer is None:
return
for rect in self.dirty_rects:
surface.blit(self.bg_buffer, rect.topleft, rect)
def flip(self, screen):
"""Update only dirty areas of the display."""
if self.need_full_redraw or not self.dirty_rects:
pygame.display.flip()
self.need_full_redraw = False
else:
pygame.display.update(self.dirty_rects)
self.dirty_rects.clear()
# -- Game setup -------------------------------------------------------------
screen = pygame.display.set_mode((GAME_WIDTH, GAME_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 21")
# Use convert() on the screen surface for faster blitting
clock = pygame.time.Clock()
hud_font = pygame.font.SysFont("monospace", 12)
big_font = pygame.font.SysFont("monospace", 48, bold=True)
medium_font = pygame.font.SysFont("monospace", 24)
small_font = pygame.font.SysFont("monospace", 12)
perf = PerfMonitor()
dirty_renderer = DirtyRectRenderer()
paddle = Paddle()
ball = Ball()
bricks = create_brick_group()
paddle_group = pygame.sprite.GroupSingle(paddle)
ball_group = pygame.sprite.GroupSingle(ball)
ball_trail = BallTrail()
paddle_glow = PaddleGlow()
shatter_animations = []
particles = ParticleSystem()
lives = MAX_LIVES
score = 0
game_state = STATE_MENU
master_volume = 0.5
sound_muted = False
menu_blink_timer = 0.0
show_perf = True
def play_sound(sound):
if not sound_muted:
sound.set_volume(master_volume)
sound.play()
def start_game():
global lives, score, bricks, game_state, shatter_animations
lives = MAX_LIVES
score = 0
bricks = create_brick_group()
ball.reset()
ball_trail.reset()
shatter_animations = []
particles.clear()
dirty_renderer.need_full_redraw = True
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, (GAME_WIDTH // 2 - title.get_width() // 2,
GAME_HEIGHT // 3 - 60))
subtitle = medium_font.render("Optimization Edition", True,
(200, 200, 255))
screen.blit(subtitle, (GAME_WIDTH // 2 - subtitle.get_width() // 2,
GAME_HEIGHT // 3 - 10))
bcount = BRICK_ROWS * BRICK_COLS
info = medium_font.render("{} bricks stress test".format(bcount),
True, (150, 150, 150))
screen.blit(info, (GAME_WIDTH // 2 - info.get_width() // 2,
GAME_HEIGHT // 3 + 25))
if int(menu_blink_timer * 2) % 2 == 0:
start = medium_font.render("Press SPACE to start", True,
(255, 255, 255))
screen.blit(start, (GAME_WIDTH // 2 - start.get_width() // 2,
GAME_HEIGHT // 2 + 30))
hints = small_font.render(
"[D] Toggle dirty rect mode [F] Toggle perf display",
True, (100, 100, 100))
screen.blit(hints, (GAME_WIDTH // 2 - hints.get_width() // 2,
GAME_HEIGHT - 30))
def draw_hud():
score_surf = hud_font.render("Score: {}".format(score),
True, (255, 255, 100))
screen.blit(score_surf, (10, GAME_HEIGHT - 20))
lives_surf = hud_font.render("Lives: {}".format(lives),
True, (255, 100, 100))
screen.blit(lives_surf,
(GAME_WIDTH // 2 - lives_surf.get_width() // 2,
GAME_HEIGHT - 20))
mode_text = "DIRTY RECT" if dirty_renderer.enabled else "FULL REDRAW"
mode_surf = hud_font.render(
"Mode: {} [D] toggle [F] perf".format(mode_text),
True, (150, 150, 150))
screen.blit(mode_surf,
(GAME_WIDTH - mode_surf.get_width() - 10,
GAME_HEIGHT - 20))
perf.count_draw(3) # HUD elements
def draw_overlay(title_text, title_color):
overlay = pygame.Surface((GAME_WIDTH, GAME_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, (GAME_WIDTH // 2 - t.get_width() // 2,
GAME_HEIGHT // 2 - 40))
fs = medium_font.render("Final score: {}".format(score),
True, (255, 255, 255))
screen.blit(fs, (GAME_WIDTH // 2 - fs.get_width() // 2,
GAME_HEIGHT // 2 + 20))
r = medium_font.render("Press SPACE for menu", True, (200, 200, 200))
screen.blit(r, (GAME_WIDTH // 2 - r.get_width() // 2,
GAME_HEIGHT // 2 + 60))
# -- Main loop --------------------------------------------------------------
running = True
while running:
dt = clock.tick(FPS) / 1000.0
perf.begin_frame()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
if game_state == STATE_PLAYING:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
game_state = STATE_PLAYING
else:
running = False
if event.key == pygame.K_F1:
sound_muted = not sound_muted
if event.key == pygame.K_d:
dirty_renderer.toggle()
if event.key == pygame.K_f:
perf.visible = not perf.visible
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:
# Track previous positions for dirty rects
old_ball_rect = ball.rect.copy()
old_paddle_rect = paddle.rect.copy()
paddle_group.update()
ball.update(dt)
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]
# Mark dirty rects
if dirty_renderer.enabled:
dirty_renderer.mark_dirty(old_ball_rect.inflate(20, 20))
dirty_renderer.mark_dirty(ball.rect.inflate(20, 20))
dirty_renderer.mark_dirty(old_paddle_rect.inflate(16, 16))
dirty_renderer.mark_dirty(paddle.rect.inflate(16, 16))
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(
GAME_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
dirty_renderer.need_full_redraw = True
if lives <= 0:
game_state = STATE_GAME_OVER
play_sound(snd_game_over)
add_high_score(score)
else:
play_sound(snd_lose_life)
ball.reset()
ball_trail.reset()
if ball.bounce_off_paddle(paddle):
play_sound(snd_paddle)
paddle_glow.trigger()
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_animations.append(
BrickShatter(brick.rect, brick.color))
particles.emit_brick_explosion(brick.rect, brick.color, 5)
if dirty_renderer.enabled:
dirty_renderer.mark_dirty(brick.rect)
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)
add_high_score(score)
# -- Draw --
perf.total_bricks = len(bricks)
if game_state == STATE_MENU:
draw_menu()
perf.count_draw(5)
elif game_state == STATE_PLAYING:
if dirty_renderer.enabled and not dirty_renderer.need_full_redraw:
# Dirty rect mode: restore background, then draw dynamic objects
dirty_renderer.restore_dirty(screen)
# Redraw bricks only in dirty areas
for brick in bricks:
for dr in dirty_renderer.dirty_rects:
if brick.rect.colliderect(dr):
screen.blit(brick.image, brick.rect)
perf.count_draw()
break
# Dynamic objects
for anim in shatter_animations:
anim.draw(screen)
if dirty_renderer.enabled:
dirty_renderer.mark_dirty(anim.rect)
ball_trail.draw(screen, perf)
paddle_glow.draw(screen, paddle.rect, perf)
paddle_group.draw(screen)
perf.count_draw()
ball_group.draw(screen)
perf.count_draw()
particles.draw(screen, perf)
else:
# Full redraw mode
screen.fill(BG_COLOR)
bricks.draw(screen)
perf.count_draw(len(bricks))
for anim in shatter_animations:
anim.draw(screen)
ball_trail.draw(screen, perf)
paddle_glow.draw(screen, paddle.rect, perf)
paddle_group.draw(screen)
perf.count_draw()
ball_group.draw(screen)
perf.count_draw()
particles.draw(screen, perf)
# Capture background for dirty rect mode
if dirty_renderer.enabled:
# Capture just the static portion (bg + bricks)
bg_snap = pygame.Surface((GAME_WIDTH, GAME_HEIGHT))
bg_snap.fill(BG_COLOR)
bricks.draw(bg_snap)
dirty_renderer.capture_background(bg_snap)
draw_hud()
info = small_font.render(
"[M] Mouse [P] Pause [D] DirtyRect [F] Perf [ESC] Pause",
True, (120, 120, 120))
screen.blit(info, (10, GAME_HEIGHT - 32))
elif game_state == STATE_PAUSED:
screen.fill(BG_COLOR)
bricks.draw(screen)
perf.count_draw(len(bricks))
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_overlay("PAUSED", (200, 200, 255))
elif game_state == STATE_GAME_OVER:
screen.fill(BG_COLOR)
bricks.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)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_overlay("YOU WIN!", (100, 255, 100))
# Performance overlay (always on top)
perf.draw(screen, small_font, dirty_renderer.enabled,
x=GAME_WIDTH - 350, y=5)
perf.end_frame(dt)
# Display update
if dirty_renderer.enabled and game_state == STATE_PLAYING:
dirty_renderer.flip(screen)
else:
pygame.display.flip()
pygame.quit()
sys.exit()
Telechargement
Telecharger le code source de ce chapitre : brick-breaker-ch21.zip
