Créer un Arkanoid en Python avec Pygame – Guide complet.

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

Capture d'écran de Pyrkanoid, clone d'Arkanoid créé en Python avec Pygame : briques colorées, raquette, balle et panneau de score

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

  1. Architecture du projet
  2. Constantes et géométrie de la grille
  3. Les 4 types de briques
  4. Les 5 niveaux
  5. La classe Ball — physique et réflexions
  6. La classe Paddle
  7. Les bonus (Powerups)
  8. Le moteur audio procédural
  9. La classe Game — logique centrale
  10. Lancer le jeu
  11. Aller plus loin

1. Architecture du projet

Pyrkanoid repose sur 6 classes aux responsabilités bien séparées :

ClasseRôle
SoundEngineGénère tous les sons au démarrage avec numpy. Aucun fichier .wav.
BallPosition, vitesse, rebonds, état stuck (Catch). Plusieurs instances pour le Multiball.
PaddleRaquette dessinée en code, gestion de la largeur (Expand), déplacement.
BrickType, HP, couleur selon l'état, dessin avec effets visuels (fissures, triangles).
PowerupCapsule tombante, collision avec la raquette, activation de l'effet.
LaserBoltProjectile du bonus Laser, montée verticale, collision avec les briques.
GameBoucle 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 :

CodeTypeHPComportement visuel
NNormal1Couleur de la palette du niveau
HHard2Devient marron foncé + croix de fissures après 1 hit
UUltra3Change de couleur à chaque hit (3 couleurs). Petits triangles indiquant les HP restants.
WWallGris, 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 :

CodeNomDuréeEffet
[E]Expand10 sRaquette plus large
[S]Slow8 sBalle ralentie à 60% de sa vitesse
[M]MultiballInstantané2 balles supplémentaires spawnées
[L]Laser12 sEspace tire des lasers depuis la raquette
[C]Catch8 sLa balle colle à la raquette au contact, relâche avec Espace
[+]Extra LifeInstantané+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.music et 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é !

Vous pouvez le telecharger ici

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *