Ce que vous allez apprendre : créer un clone d'Arkanoid complet en Python avec Pygame — 4 types de briques, 5 niveaux, 6 bonus, sons procéduraux, physique de balle avancée. Aucun asset externe requis.
Vous pouvez le telecharger ici

Introduction
Arkanoid est l'un des classiques du jeu vidéo des années 80, dérivé du Breakout d'Atari. Raquette, balle, briques, bonus qui tombent — une formule simple mais redoutablement addictive. C'est aussi un excellent projet pour progresser en Python : physique de balle, détection de collision, machines à états, génération audio procédurale.
Dans ce tutoriel, vous allez construire Pyrkanoid, un clone fidèle et jouable avec :
- 4 types de briques (normale, dure, ultra, indestructible)
- 5 niveaux aux layouts distincts (pyramide, damier, forteresse, diamant, chaos)
- 6 bonus classiques : Expand, Slow, Multiball, Laser, Catch, Extra Life
- Tous les sons générés en Python avec numpy — aucun fichier audio externe
- Physique de balle avec angle de réflexion variable selon la zone de la raquette
Prérequis : Python 3.8+, bases de la POO. Installez les dépendances avec : pip install pygame numpy
Sommaire
- Architecture du projet
- Constantes et géométrie de la grille
- Les 4 types de briques
- Les 5 niveaux
- La classe Ball — physique et réflexions
- La classe Paddle
- Les bonus (Powerups)
- Le moteur audio procédural
- La classe Game — logique centrale
- Lancer le jeu
- Aller plus loin
1. Architecture du projet
Pyrkanoid repose sur 6 classes aux responsabilités bien séparées :
| Classe | Rôle |
|---|---|
SoundEngine | Génère tous les sons au démarrage avec numpy. Aucun fichier .wav. |
Ball | Position, vitesse, rebonds, état stuck (Catch). Plusieurs instances pour le Multiball. |
Paddle | Raquette dessinée en code, gestion de la largeur (Expand), déplacement. |
Brick | Type, HP, couleur selon l'état, dessin avec effets visuels (fissures, triangles). |
Powerup | Capsule tombante, collision avec la raquette, activation de l'effet. |
LaserBolt | Projectile du bonus Laser, montée verticale, collision avec les briques. |
Game | Boucle principale, machine à états, orchestration de tout le reste. |
Tout tient dans un seul fichier arkanoid.py, sans asset externe. On lance avec python arkanoid.py.
2. Constantes et géométrie de la grille
La fenêtre fait 480×640 px. Une barre HUD de 50 px en haut affiche score, niveau et vies. La grille de briques est calculée automatiquement pour être centrée horizontalement :
WIDTH, HEIGHT = 480, 640
HUD_HEIGHT = 50
FPS = 60
BRICK_COLS = 10
BRICK_ROWS = 8
BRICK_W = 44
BRICK_H = 20
BRICK_PAD = 2
# Centrage automatique de la grille
BRICK_OFFSET_X = (WIDTH - BRICK_COLS * (BRICK_W + BRICK_PAD) + BRICK_PAD) // 2
BRICK_OFFSET_Y = HUD_HEIGHT + 20
Chaque brique connaît sa colonne et sa ligne, et calcule sa position pixel à la demande via sa méthode rect(). Aucune position absolue n'est stockée — tout découle des indices.
3. Les 4 types de briques
La classe Brick gère 4 types définis par des constantes :
| Code | Type | HP | Comportement visuel |
|---|---|---|---|
N | Normal | 1 | Couleur de la palette du niveau |
H | Hard | 2 | Devient marron foncé + croix de fissures après 1 hit |
U | Ultra | 3 | Change de couleur à chaque hit (3 couleurs). Petits triangles indiquant les HP restants. |
W | Wall | ∞ | Gris, aspect surélevé, jamais détruit |
class Brick:
def hit(self):
if self.kind == WALL:
return 'wall' # rebond sans dommage
self.hp -= 1
if self.hp <= 0:
self.alive = False
return 'destroyed'
return 'damaged'
def current_color(self):
if self.kind == ULTRA:
idx = self.max_hp - self.hp # 0, 1 ou 2 selon les hits reçus
return ULTRA_COLORS[idx] # violet → orange → cyan
if self.kind == HARD:
return HARD_CRACKED if self.hp < self.max_hp else HARD_COLOR
# ...
Le retour de hit() — 'destroyed', 'damaged' ou 'wall' — permet à Game de jouer le bon son et de décider si un bonus doit tomber, sans que Brick ait à connaître ces mécaniques.
L'effet visuel des briques est entièrement dessiné avec des primitives Pygame. Chaque brique a un highlight en haut/gauche et une ombre en bas/droite pour l'effet 3D. Les briques HARD affichent deux lignes diagonales croisées quand elles sont endommagées. Les briques ULTRA affichent de petits triangles blancs indiquant les HP restants :
# Fissures sur brique HARD endommagée
if self.kind == HARD and self.hp < self.max_hp:
crack_col = (60, 30, 10)
pygame.draw.line(surf, crack_col, (r.x+4, r.y+4), (r.x+r.w-4, r.y+r.h-4), 2)
pygame.draw.line(surf, crack_col, (r.x+r.w-4, r.y+4), (r.x+4, r.y+r.h-4), 2)
# Triangles HP sur brique ULTRA
if self.kind == ULTRA:
for i in range(self.hp):
cx = r.x + 5 + i * 8
cy = r.y + r.h - 5
pygame.draw.polygon(surf, WHITE, [(cx, cy-3),(cx-3,cy+3),(cx+3,cy+3)])
4. Les 5 niveaux
Chaque niveau est une grille 10×8 codée en dur comme liste de listes. Les codes sont directement les constantes de type de brique. Cette approche est lisible et facile à modifier :
# Level 1 — Pyramide (briques normales uniquement)
[
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,'N',0,0,0,0,0],
[0,0,0,'N','N','N',0,0,0,0],
[0,0,'N','N','N','N','N',0,0,0],
[0,'N','N','N','N','N','N','N',0,0],
['N','N','N','N','N','N','N','N','N','N'],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
],
Les 5 niveaux progressent en difficulté :
- Niveau 1 — Pyramide : uniquement des briques normales disposées en triangle. Introduction en douceur.
- Niveau 2 — Damier : briques normales et dures en alternance. Beaucoup d'espaces vides, la balle voyage plus librement.
- Niveau 3 — Forteresse : un cadre de briques WALL indestructibles entoure des briques normales et dures, avec des ULTRA au centre. Le niveau a été revu pour rester jouable malgré les murs.
- Niveau 4 — Diamant : disposition en losange avec WALL en obstacles internes, mix de tous les types destructibles.
- Niveau 5 — Chaos : grille dense, tous les types présents, une colonne et une ligne de WALL formant une croix. Le plus difficile.
La condition de victoire ignore les briques WALL : any(b.alive and b.kind != WALL for b in self.bricks). Un niveau est gagné dès que toutes les briques destructibles sont éliminées.
5. La classe Ball — physique et réflexions
C'est le cœur du gameplay. La balle stocke sa position en flottants pour une physique précise, et ne convertit en entiers qu'au moment du dessin.
5.1 Réflexion sur la raquette avec angle variable
C'est la mécanique la plus importante de tout jeu Arkanoid. La balle ne rebondit pas à angle constant — elle part dans la direction que le joueur choisit selon la zone de la raquette touchée :
def _ball_paddle_collide(self, ball):
# Position relative sur la raquette : -1 (bord gauche) à +1 (bord droit)
rel = (ball.x - self.paddle.x) / (self.paddle.w / 2)
rel = max(-0.95, min(0.95, rel)) # clamp pour éviter les angles trop rasants
angle = rel * 65 # de -65° à +65° par rapport à la verticale
rad = math.radians(angle - 90) # -90 = droit vers le haut au centre
spd = ball.speed()
ball.vx = spd * math.cos(rad)
ball.vy = -abs(spd * math.sin(rad)) # toujours vers le haut
Le -abs() sur vy est crucial : il empêche la balle de traverser la raquette vers le bas en cas de collision à grande vitesse ou de déplacement rapide de la raquette.
5.2 Réflexion sur les briques
Pour chaque collision balle-brique, on calcule les 4 "overlaps" (chevauchements) entre le rectangle de la balle et celui de la brique. Le plus petit overlap indique de quel côté vient la collision et donc quel axe inverser :
over_left = ball.x - r.left
over_right = r.right - ball.x
over_top = ball.y - r.top
over_bottom = r.bottom - ball.y
min_over = min(over_left, over_right, over_top, over_bottom)
if min_over == over_top or min_over == over_bottom:
ball.vy = -ball.vy # collision verticale → on inverse vy
else:
ball.vx = -ball.vx # collision horizontale → on inverse vx
5.3 Accélération progressive
Toutes les 5 briques détruites, la vitesse de toutes les balles augmente légèrement :
SPEED_UP_EVERY = 5
SPEED_BUMP = 0.15
if self.bricks_destroyed % self.SPEED_UP_EVERY == 0:
self.speed_multiplier += self.SPEED_BUMP
for b in self.balls:
b.set_speed(b.speed() + self.SPEED_BUMP)
5.4 Multiball
Le bonus Multiball spawn 2 balles supplémentaires avec des angles aléatoires. Une vie n'est perdue que quand toutes les balles sont sorties :
active_balls = [b for b in self.balls if b.active]
self.balls = active_balls
if len(self.balls) == 0:
self._handle_ball_loss() # on perd une vie seulement ici
6. La classe Paddle
La raquette est entièrement dessinée avec des primitives Pygame — rounded rect, bordure sombre, ligne de highlight sur le dessus pour l'effet 3D :
def draw(self, surf):
r = self.rect()
pygame.draw.rect(surf, self.color, r, border_radius=6)
pygame.draw.rect(surf, (40, 80, 140), r, 2, border_radius=6) # bordure
highlight = pygame.Rect(r.x + 4, r.y + 2, r.w - 8, 3)
pygame.draw.rect(surf, (160, 210, 255), highlight, border_radius=2)
La largeur bascule entre BASE_W = 80 et EXPAND_W = 120 selon le bonus Expand. Quand le timer Expand expire, la raquette revient automatiquement à sa taille normale.
7. Les bonus (Powerups)
Chaque brique détruite a 20% de chance de lâcher un bonus. Les bonus tombent sous forme de capsules colorées avec une lettre. Voici les 6 bonus implémentés :
| Code | Nom | Durée | Effet |
|---|---|---|---|
[E] | Expand | 10 s | Raquette plus large |
[S] | Slow | 8 s | Balle ralentie à 60% de sa vitesse |
[M] | Multiball | Instantané | 2 balles supplémentaires spawnées |
[L] | Laser | 12 s | Espace tire des lasers depuis la raquette |
[C] | Catch | 8 s | La balle colle à la raquette au contact, relâche avec Espace |
[+] | Extra Life | Instantané | +1 vie (maximum 5) |
Les bonus à durée limitée affichent une barre de progression sous le HUD. Le timer est décompté en secondes réelles grâce au dt de la boucle principale :
def _tick_timers(self, dt):
for k in self.pu_timers:
if self.pu_timers[k] > 0:
self.pu_timers[k] -= dt
if self.pu_timers[k] <= 0:
self.pu_timers[k] = 0
if k == PU_EXPAND:
self.paddle.w = Paddle.BASE_W # retour à la taille normale
elif k == PU_LASER:
self.laser_active = False
elif k == PU_CATCH:
self.catch_active = False
8. Le moteur audio procédural
C'est l'une des parties les plus intéressantes du projet. Tous les sons sont générés au démarrage avec numpy — pas un seul fichier .wav à gérer.
Le principe : on génère un tableau numpy de samples audio, on l'enveloppe avec un fondu pour éviter les clics, et on le convertit en son Pygame via pygame.sndarray.make_sound() :
def _make_tone(self, freq, duration, volume=0.5, wave='sine', fade_out=True):
n = int(self.sample_rate * duration)
t = np.linspace(0, duration, n, endpoint=False)
if wave == 'sine':
samples = np.sin(2 * np.pi * freq * t)
elif wave == 'square':
samples = np.sign(np.sin(2 * np.pi * freq * t))
elif wave == 'sawtooth':
samples = 2 * (t * freq - np.floor(t * freq + 0.5))
if fade_out:
envelope = np.linspace(1.0, 0.0, n)
samples = samples * envelope
mono = (samples * volume * 32767).astype(np.int16)
stereo = np.column_stack((mono, mono))
return pygame.sndarray.make_sound(stereo)
Pour les sons en plusieurs notes (chime de bonus, fanfare de niveau, game over), on concatène plusieurs tableaux :
# Chime de collecte de bonus : do-mi-sol ascendant
self.sounds['powerup'] = self._concat_sounds([
(523, 0.08, 0.5, 'sine'), # do
(659, 0.08, 0.5, 'sine'), # mi
(784, 0.12, 0.5, 'sine'), # sol
])
Les 3 formes d'onde ont des caractères différents : sine = son pur et rond (rebonds, chimes), square = son électronique et dur (briques), sawtooth = son agressif et riche en harmoniques (briques ultra). Ce choix donne à chaque événement une signature sonore distincte.
9. La classe Game — logique centrale
9.1 Machine à états
Game utilise une machine à états simple avec 5 états :
STATE_PLAY = 'play' # jeu en cours
STATE_BETWEEN = 'between' # écran de fin de niveau
STATE_PAUSE = 'pause' # pause
STATE_GAMEOVER = 'gameover' # game over
STATE_WIN = 'win' # victoire (après niveau 5)
Chaque état conditionne ce que font update(), draw() et handle_event(). C'est propre et facile à étendre.
9.2 Séparation reset_game / _load_level
reset_game() remet tout à zéro (score, vies, niveau). _load_level() charge seulement le niveau courant. Quand on passe au niveau suivant, on appelle uniquement _load_level() — le score et les vies sont préservés.
9.3 Le laser
Le bonus Laser est une mécanique classique de l'Arkanoid original. La raquette tire deux projectiles depuis ses tiers gauche et droit :
# Dans handle_event, touche Espace
if not released and self.laser_active:
for ox in [-self.paddle.w//3, self.paddle.w//3]:
self.lasers.append(LaserBolt(
self.paddle.x + ox,
self.paddle.top
))
self.sound.play('laser')
Les lasers montent à vitesse fixe (-10 px/frame), détruisent la première brique touchée et disparaissent. Ils interagissent avec tous les types de briques, y compris les ULTRA et les HARD.
10. Lancer le jeu
pip install pygame numpy
python arkanoid.py
Contrôles :
- ← → : déplacer la raquette
- Espace : lancer la balle / tirer laser / relâcher balle collée (Catch)
- P : pause / reprendre
- R : recommencer (game over ou victoire)
11. Aller plus loin
Pyrkanoid est un jeu complet, mais voici des idées pour continuer à progresser :
- Éditeur de niveaux : un simple fichier texte ou JSON pour définir les grilles sans toucher au code
- Bonus supplémentaires : balle de feu (traverse les briques), bouclier en bas de l'écran, balle lente mais grosse
- Animations : flash de brique à la destruction, particules, screen shake sur la perte d'une vie
- Meilleur score persistant avec
json - Musique de fond avec
pygame.mixer.musicet une mélodie générée avec numpy - Menu principal et sélection de niveau
Le code complet (~550 lignes) est disponible dans cet article. Modifiez les grilles de niveaux, ajoutez vos propres bonus et partagez votre version en commentaire !
Conclusion
Vous avez maintenant un clone d'Arkanoid complet en Python avec Pygame. Ce projet couvre des techniques importantes : physique de balle avec angle variable, détection de collision par overlap, machine à états de jeu, génération audio procédurale avec numpy, et gestion de plusieurs entités dynamiques (balles, bonus, lasers). Toutes ces compétences sont directement réutilisables dans vos prochains projets de jeux.
Si ce tutoriel vous a aidé, partagez-le et laissez un commentaire avec votre niveau préféré !