Ce que vous allez apprendre
Dans ce chapitre, nous ajoutons la gestion de l'ecran : bascule plein ecran avec F11, mise a l'echelle de la resolution, gestion du redimensionnement de la fenetre, et maintien du ratio d'aspect avec barres noires (letterboxing/pillarboxing).
Le concept
Le jeu dessine toujours sur une surface fixe (800x600). Cette surface est ensuite mise a l'echelle pour s'adapter a la fenetre, quelle que soit sa taille. Le ratio 4:3 est preserve automatiquement.
Cette technique de resolution virtuelle est utilisee dans de nombreux jeux professionnels pour garantir un rendu coherent sur tous les ecrans.
Etape par etape
1. La classe ScreenManager
Le ScreenManager encapsule toute la logique d'ecran. Il possede une game_surface de taille fixe et calcule un rectangle de destination pour la mise a l'echelle.
class ScreenManager:
def __init__(self):
self.game_surface = pygame.Surface(
(GAME_WIDTH, GAME_HEIGHT))
self.fullscreen = False
self.screen = pygame.display.set_mode(
(GAME_WIDTH, GAME_HEIGHT),
pygame.RESIZABLE)
2. Calcul du ratio d'aspect
La methode _update_scaling() compare le ratio de la fenetre a celui du jeu pour determiner s'il faut des bandes noires verticales (pillarbox) ou horizontales (letterbox).
3. Conversion des coordonnees souris
Les coordonnees de la souris dans la fenetre doivent etre converties en coordonnees de la surface de jeu. La methode window_to_game() effectue ce calcul en tenant compte de la position et de la taille du rectangle mis a l'echelle.
def window_to_game(self, wx, wy):
gx = (wx - self.scaled_rect.x) * GAME_WIDTH \
/ self.scaled_rect.width
gy = (wy - self.scaled_rect.y) * GAME_HEIGHT \
/ self.scaled_rect.height
return (int(gx), int(gy))
4. Plein ecran
F11 bascule entre le mode fenetre et le plein ecran. Pygame propose le flag FULLSCREEN qui utilise toute la surface de l'ecran. Le calcul de mise a l'echelle est ensuite mis a jour.
5. Affichage avec smoothscale
La methode present() utilise pygame.transform.smoothscale pour un rendu lisse. L'ecran est d'abord rempli de noir (pour les barres), puis la surface de jeu mise a l'echelle est blit au centre.
Points cles a retenir
Astuce : dessinez toujours sur la surface de jeu fixe, jamais directement sur l'ecran. Cela simplifie le code de gameplay.
Piege courant : oublier de convertir les coordonnees de la souris provoque des clics decales quand la fenetre est redimensionnee.
Code complet
"""Brick Breaker - Chapter 17: Screen Management"""
import pygame
import sys
import math
import array
import random
import json
import os
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
# Internal (logical) resolution -- the game always renders to this size
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
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
STATE_DIFFICULTY = 5
STATE_HIGH_SCORES = 6
# Difficulty
DIFFICULTY_EASY = 0
DIFFICULTY_NORMAL = 1
DIFFICULTY_HARD = 2
DIFFICULTY_NAMES = ["Easy", "Normal", "Hard"]
DIFFICULTY_SPEEDS = [200, 300, 420]
# 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)
snd_click = generate_beep(frequency=600, duration_ms=40, volume=0.2)
# -- 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)
# -- High Scores -----------------------------------------------------------
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, difficulty):
scores = load_high_scores()
scores.append({"score": new_score,
"difficulty": DIFFICULTY_NAMES[difficulty]})
scores = sorted(scores, key=lambda s: s["score"], reverse=True)[:10]
save_high_scores(scores)
return scores
# -- Screen management ------------------------------------------------------
class ScreenManager:
"""Manages window, fullscreen toggle, and resolution scaling.
The game always renders to a fixed-size surface (GAME_WIDTH x GAME_HEIGHT).
That surface is then scaled to fit the window while keeping the aspect
ratio. Black bars (letter-boxing / pillar-boxing) fill any extra space.
"""
def __init__(self):
self.game_surface = pygame.Surface((GAME_WIDTH, GAME_HEIGHT))
self.fullscreen = False
self.window_width = GAME_WIDTH
self.window_height = GAME_HEIGHT
self.screen = pygame.display.set_mode(
(self.window_width, self.window_height), pygame.RESIZABLE)
pygame.display.set_caption("Brick Breaker - Chapter 17")
self._update_scaling()
def _update_scaling(self):
"""Recalculate the scaled rect that keeps the aspect ratio."""
win_w, win_h = self.screen.get_size()
game_aspect = GAME_WIDTH / GAME_HEIGHT
win_aspect = win_w / win_h
if win_aspect > game_aspect:
# Window is wider than game -- pillar-box
scaled_h = win_h
scaled_w = int(win_h * game_aspect)
else:
# Window is taller than game -- letter-box
scaled_w = win_w
scaled_h = int(win_w / game_aspect)
self.scaled_rect = pygame.Rect(
(win_w - scaled_w) // 2,
(win_h - scaled_h) // 2,
scaled_w, scaled_h)
def toggle_fullscreen(self):
self.fullscreen = not self.fullscreen
if self.fullscreen:
self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
else:
self.screen = pygame.display.set_mode(
(self.window_width, self.window_height), pygame.RESIZABLE)
self._update_scaling()
def handle_resize(self, event):
"""Call when a VIDEORESIZE event is received."""
if not self.fullscreen:
self.window_width = event.w
self.window_height = event.h
self.screen = pygame.display.set_mode(
(event.w, event.h), pygame.RESIZABLE)
self._update_scaling()
def present(self):
"""Scale the game surface to the window and flip."""
self.screen.fill((0, 0, 0))
scaled = pygame.transform.smoothscale(
self.game_surface,
(self.scaled_rect.width, self.scaled_rect.height))
self.screen.blit(scaled, self.scaled_rect.topleft)
pygame.display.flip()
def window_to_game(self, wx, wy):
"""Convert window coordinates to game-surface coordinates."""
gx = (wx - self.scaled_rect.x) * GAME_WIDTH / self.scaled_rect.width
gy = (wy - self.scaled_rect.y) * GAME_HEIGHT / self.scaled_rect.height
return (int(gx), int(gy))
# -- Button class -----------------------------------------------------------
class Button:
def __init__(self, x, y, width, height, label, font,
color=(80, 80, 120), hover_color=(120, 120, 180),
text_color=(255, 255, 255)):
self.rect = pygame.Rect(x, y, width, height)
self.label = label
self.font = font
self.color = color
self.hover_color = hover_color
self.text_color = text_color
self.hovered = False
self.clicked = False
def update(self, mouse_pos, mouse_click):
self.hovered = self.rect.collidepoint(mouse_pos)
self.clicked = self.hovered and mouse_click
return self.clicked
def draw(self, surface):
current_color = self.hover_color if self.hovered else self.color
shadow_rect = self.rect.move(3, 3)
pygame.draw.rect(surface, (0, 0, 0, 100), shadow_rect,
border_radius=6)
pygame.draw.rect(surface, current_color, self.rect, border_radius=6)
border_color = (200, 200, 255) if self.hovered else (100, 100, 150)
pygame.draw.rect(surface, border_color, self.rect, width=2,
border_radius=6)
text_surf = self.font.render(self.label, True, self.text_color)
text_rect = text_surf.get_rect(center=self.rect.center)
surface.blit(text_surf, text_rect)
# -- Particle System --------------------------------------------------------
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
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:
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):
cx, cy = rect.centerx, 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)
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):
cx, cy = ball_x, paddle_rect.top
for _ in range(10):
angle = random.uniform(-math.pi, 0)
speed = random.uniform(40, 150)
vx = speed * math.cos(angle)
vy = speed * math.sin(angle)
lifetime = random.uniform(0.2, 0.5)
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):
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:
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:
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):
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:
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 = GAME_WIDTH // 2
self.rect.y = GAME_HEIGHT - 40
self.use_mouse = False
def update(self, mouse_game_x=None):
if self.use_mouse and mouse_game_x is not None:
self.rect.centerx = mouse_game_x
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)
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):
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 -------------------------------------------------------------
scr_mgr = ScreenManager()
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)
button_font = pygame.font.SysFont("monospace", 22, bold=True)
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
level = 1
game_state = STATE_MENU
current_difficulty = DIFFICULTY_NORMAL
master_volume = 0.5
sound_muted = False
menu_blink_timer = 0.0
# -- Buttons ----------------------------------------------------------------
btn_start = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 10,
200, 45, "Start", button_font,
color=(40, 100, 60), hover_color=(60, 150, 90))
btn_difficulty = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 65,
200, 45, "Difficulty", button_font)
btn_high_scores = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 120,
200, 45, "High Scores", button_font)
btn_quit = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 175,
200, 45, "Quit", button_font,
color=(120, 40, 40), hover_color=(180, 60, 60))
menu_buttons = [btn_start, btn_difficulty, btn_high_scores, btn_quit]
btn_easy = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 - 30,
200, 45, "Easy", button_font,
color=(40, 120, 40), hover_color=(60, 180, 60))
btn_normal = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 30,
200, 45, "Normal", button_font,
color=(120, 120, 40), hover_color=(180, 180, 60))
btn_hard = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 90,
200, 45, "Hard", button_font,
color=(120, 40, 40), hover_color=(180, 60, 60))
btn_diff_back = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 160,
200, 45, "Back", button_font)
difficulty_buttons = [btn_easy, btn_normal, btn_hard, btn_diff_back]
btn_resume = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 - 20,
200, 45, "Resume", button_font,
color=(40, 100, 60), hover_color=(60, 150, 90))
btn_restart = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 40,
200, 45, "Restart", button_font,
color=(120, 120, 40), hover_color=(180, 180, 60))
btn_pause_quit = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 100,
200, 45, "Quit to Menu", button_font,
color=(120, 40, 40), hover_color=(180, 60, 60))
pause_buttons = [btn_resume, btn_restart, btn_pause_quit]
btn_hs_back = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT - 80,
200, 45, "Back", button_font)
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.speed = DIFFICULTY_SPEEDS[current_difficulty]
angle = math.radians(-60)
ball.vx = ball.speed * math.cos(angle)
ball.vy = ball.speed * math.sin(angle)
ball_trail.reset()
shatter_animations = []
particles.clear()
game_state = STATE_PLAYING
# -- Draw helpers -----------------------------------------------------------
def draw_menu(surface):
global menu_blink_timer
menu_blink_timer += clock.get_time() / 1000.0
surface.fill(BG_COLOR)
title = big_font.render("BRICK BREAKER", True, (255, 200, 50))
surface.blit(title, (GAME_WIDTH // 2 - title.get_width() // 2,
GAME_HEIGHT // 3 - 60))
subtitle = medium_font.render("Screen Management", True, (200, 200, 255))
surface.blit(subtitle, (GAME_WIDTH // 2 - subtitle.get_width() // 2,
GAME_HEIGHT // 3 - 10))
diff_text = small_font.render(
"Difficulty: {}".format(DIFFICULTY_NAMES[current_difficulty]),
True, (180, 180, 180))
surface.blit(diff_text,
(GAME_WIDTH // 2 - diff_text.get_width() // 2,
GAME_HEIGHT // 3 + 20))
fs_hint = small_font.render("[F11] Toggle fullscreen", True,
(100, 100, 100))
surface.blit(fs_hint, (GAME_WIDTH // 2 - fs_hint.get_width() // 2,
GAME_HEIGHT - 30))
def draw_difficulty_screen(surface):
surface.fill(BG_COLOR)
title = big_font.render("DIFFICULTY", True, (255, 200, 50))
surface.blit(title, (GAME_WIDTH // 2 - title.get_width() // 2,
GAME_HEIGHT // 4 - 30))
current = small_font.render(
"Current: {}".format(DIFFICULTY_NAMES[current_difficulty]),
True, (200, 200, 200))
surface.blit(current, (GAME_WIDTH // 2 - current.get_width() // 2,
GAME_HEIGHT // 4 + 20))
def draw_high_scores_screen(surface):
surface.fill(BG_COLOR)
title = big_font.render("HIGH SCORES", True, (255, 200, 50))
surface.blit(title, (GAME_WIDTH // 2 - title.get_width() // 2, 40))
scores = load_high_scores()
if not scores:
ns = medium_font.render("No scores yet!", True, (150, 150, 150))
surface.blit(ns, (GAME_WIDTH // 2 - ns.get_width() // 2,
GAME_HEIGHT // 2 - 20))
else:
header = hud_font.render(" # Score Difficulty", True,
(200, 200, 200))
surface.blit(header, (GAME_WIDTH // 2 - 140, 110))
pygame.draw.line(surface, (100, 100, 100),
(GAME_WIDTH // 2 - 140, 135),
(GAME_WIDTH // 2 + 140, 135))
for i, entry in enumerate(scores[:10]):
color = (255, 215, 0) if i == 0 else (
(200, 200, 200) if i < 3 else (150, 150, 150))
line = "{:>3}. {:>6} {}".format(
i + 1, entry["score"], entry.get("difficulty", "---"))
ts = hud_font.render(line, True, color)
surface.blit(ts, (GAME_WIDTH // 2 - 140, 145 + i * 35))
def draw_hud(surface):
score_surf = hud_font.render("Score: {}".format(score),
True, (255, 255, 100))
surface.blit(score_surf, (10, 10))
lives_surf = hud_font.render("Lives: {}".format(lives),
True, (255, 100, 100))
surface.blit(lives_surf,
(GAME_WIDTH // 2 - lives_surf.get_width() // 2, 10))
level_surf = hud_font.render("Level: {}".format(level),
True, (100, 200, 255))
surface.blit(level_surf,
(GAME_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)
surface.blit(vol_surf,
(GAME_WIDTH - vol_surf.get_width() - 10, 30))
# Show window size info
win_w, win_h = scr_mgr.screen.get_size()
res_text = "Window: {}x{} | Game: {}x{}{}".format(
win_w, win_h, GAME_WIDTH, GAME_HEIGHT,
" [FS]" if scr_mgr.fullscreen else "")
res_surf = small_font.render(res_text, True, (100, 100, 100))
surface.blit(res_surf, (10, 30))
def draw_overlay(surface, title_text, title_color):
overlay = pygame.Surface((GAME_WIDTH, GAME_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
surface.blit(overlay, (0, 0))
t = big_font.render(title_text, True, title_color)
surface.blit(t, (GAME_WIDTH // 2 - t.get_width() // 2,
GAME_HEIGHT // 2 - 100))
fs = medium_font.render("Final score: {}".format(score),
True, (255, 255, 255))
surface.blit(fs, (GAME_WIDTH // 2 - fs.get_width() // 2,
GAME_HEIGHT // 2 - 50))
r = medium_font.render("Press SPACE for menu", True, (200, 200, 200))
surface.blit(r, (GAME_WIDTH // 2 - r.get_width() // 2,
GAME_HEIGHT // 2 - 10))
def draw_game_scene(surface):
surface.fill(BG_COLOR)
bricks.draw(surface)
for anim in shatter_animations:
anim.draw(surface)
ball_trail.draw(surface)
paddle_glow.draw(surface, paddle.rect)
paddle_group.draw(surface)
ball_group.draw(surface)
particles.draw(surface)
draw_hud(surface)
# -- Main loop --------------------------------------------------------------
running = True
while running:
dt = clock.tick(FPS) / 1000.0
# Convert mouse position to game coordinates
raw_mx, raw_my = pygame.mouse.get_pos()
mouse_pos = scr_mgr.window_to_game(raw_mx, raw_my)
mouse_click = False
# Alias the game surface for drawing
gs = scr_mgr.game_surface
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.VIDEORESIZE:
scr_mgr.handle_resize(event)
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mouse_click = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_F11:
scr_mgr.toggle_fullscreen()
if event.key == pygame.K_ESCAPE:
if game_state == STATE_PLAYING:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
game_state = STATE_PLAYING
elif game_state in (STATE_MENU, STATE_DIFFICULTY,
STATE_HIGH_SCORES):
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
# -- Button updates --
if game_state == STATE_MENU:
for btn in menu_buttons:
btn.update(mouse_pos, mouse_click)
if btn_start.clicked:
play_sound(snd_click)
start_game()
elif btn_difficulty.clicked:
play_sound(snd_click)
game_state = STATE_DIFFICULTY
elif btn_high_scores.clicked:
play_sound(snd_click)
game_state = STATE_HIGH_SCORES
elif btn_quit.clicked:
running = False
elif game_state == STATE_DIFFICULTY:
for btn in difficulty_buttons:
btn.update(mouse_pos, mouse_click)
if btn_easy.clicked:
play_sound(snd_click)
current_difficulty = DIFFICULTY_EASY
game_state = STATE_MENU
elif btn_normal.clicked:
play_sound(snd_click)
current_difficulty = DIFFICULTY_NORMAL
game_state = STATE_MENU
elif btn_hard.clicked:
play_sound(snd_click)
current_difficulty = DIFFICULTY_HARD
game_state = STATE_MENU
elif btn_diff_back.clicked:
play_sound(snd_click)
game_state = STATE_MENU
elif game_state == STATE_HIGH_SCORES:
btn_hs_back.update(mouse_pos, mouse_click)
if btn_hs_back.clicked:
play_sound(snd_click)
game_state = STATE_MENU
elif game_state == STATE_PAUSED:
for btn in pause_buttons:
btn.update(mouse_pos, mouse_click)
if btn_resume.clicked:
play_sound(snd_click)
game_state = STATE_PLAYING
elif btn_restart.clicked:
play_sound(snd_click)
start_game()
elif btn_pause_quit.clicked:
play_sound(snd_click)
game_state = STATE_MENU
# -- Update --
if game_state == STATE_PLAYING:
paddle.update(mouse_game_x=mouse_pos[0])
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]
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
if lives <= 0:
game_state = STATE_GAME_OVER
play_sound(snd_game_over)
add_high_score(score, current_difficulty)
else:
play_sound(snd_lose_life)
ball.reset()
ball.speed = DIFFICULTY_SPEEDS[current_difficulty]
angle = math.radians(-60)
ball.vx = ball.speed * math.cos(angle)
ball.vy = ball.speed * math.sin(angle)
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)
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, current_difficulty)
# -- Draw (to game surface) --
if game_state == STATE_MENU:
draw_menu(gs)
for btn in menu_buttons:
btn.draw(gs)
elif game_state == STATE_DIFFICULTY:
draw_difficulty_screen(gs)
for btn in difficulty_buttons:
btn.draw(gs)
elif game_state == STATE_HIGH_SCORES:
draw_high_scores_screen(gs)
btn_hs_back.draw(gs)
elif game_state == STATE_PLAYING:
draw_game_scene(gs)
info = small_font.render(
"[M] Mouse [P] Pause [F1] Mute [F11] Fullscreen",
True, (150, 150, 150))
gs.blit(info, (10, GAME_HEIGHT - 25))
elif game_state == STATE_PAUSED:
draw_game_scene(gs)
overlay = pygame.Surface((GAME_WIDTH, GAME_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
gs.blit(overlay, (0, 0))
t = big_font.render("PAUSED", True, (200, 200, 255))
gs.blit(t, (GAME_WIDTH // 2 - t.get_width() // 2,
GAME_HEIGHT // 2 - 120))
for btn in pause_buttons:
btn.draw(gs)
elif game_state == STATE_GAME_OVER:
draw_game_scene(gs)
draw_overlay(gs, "GAME OVER", (255, 60, 60))
elif game_state == STATE_YOU_WIN:
draw_game_scene(gs)
draw_overlay(gs, "YOU WIN!", (100, 255, 100))
# Scale and present
scr_mgr.present()
pygame.quit()
sys.exit()
Telechargement
Telecharger le code source de ce chapitre : brick-breaker-ch17.zip
