Python · Développement de jeux

Pygame

Créer des jeux 2D en Python — fenêtre, boucle de jeu, sprites, collisions, sons, HUD, et architecture avec système de scènes. Prérequis : POO Python.

Introduction & installation

🎮

Pygame est une bibliothèque Python pour créer des jeux 2D. Elle fournit tout le nécessaire : fenêtre graphique, gestion des événements clavier/souris, affichage d'images et de formes, sons, et horloge pour contrôler le framerate. Elle est idéale pour apprendre les bases du développement de jeux.

Installation
# Installation via pip
pip install pygame

# Vérifier l'installation
python -m pygame.examples.aliens  # jeu de démo inclus

# Dans VS Code — terminal intégré
pip install pygame --upgrade
Module PygameRôle
pygame.displayFenêtre et surface d'affichage
pygame.eventFile d'événements clavier/souris/fenêtre
pygame.spriteSprites et groupes de sprites
pygame.imageChargement d'images
pygame.drawDessin de formes géométriques
pygame.fontRendu de texte
pygame.mixerSons et musique
pygame.timeHorloge et FPS
pygame.mathVecteurs 2D et 3D

Fenêtre & boucle de jeu

Structure minimale d'un jeu
import pygame
from pygame.locals import *

# ── Initialisation ──────────────────────────
pygame.init()
pygame.mixer.init()

WIDTH, HEIGHT = 800, 600
FPS = 60

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Mon Jeu")
clock  = pygame.time.Clock()

# ── Couleurs ─────────────────────────────────
BLACK  = (0,   0,   0  )
WHITE  = (255, 255, 255)
RED    = (220, 50,  50 )

# ── Boucle principale ────────────────────────
running = True
while running:

    # 1. Événements
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False

    # 2. Mise à jour de la logique
    # ... (déplacements, collisions…)

    # 3. Dessin
    screen.fill(BLACK)
    # ... (draw calls)
    pygame.display.flip()  # afficher le frame

    # 4. Limiter le FPS
    clock.tick(FPS)

pygame.quit()

La boucle de jeu tourne 60 fois par seconde. À chaque itération, elle suit toujours le même ordre :

🎮 Début du frame
① Lire les événements
clavier, souris, quitter…
② Mettre à jour la logique
déplacements, IA, physique…
③ Dessiner
screen.fill → sprites → HUD
④ Afficher + limiter FPS
display.flip + clock.tick
↺ recommencer
⏱️

clock.tick(60) retourne le nombre de millisecondes écoulées depuis le dernier appel (delta time). Pour des mouvements indépendants du FPS : dt = clock.tick(60) / 1000, puis multiplier les vitesses par dt.

Événements & clavier

Gestion des événements
for event in pygame.event.get():
    # Fermer la fenêtre
    if event.type == QUIT:
        running = False

    # Touche appuyée (une seule fois)
    if event.type == KEYDOWN:
        if event.key == K_ESCAPE:
            running = False
        if event.key == K_SPACE:
            player.jump()
        if event.key == K_r:
            game.reset()

    # Touche relâchée
    if event.type == KEYUP:
        if event.key == K_SPACE:
            player.stop_jump()

    # Clic souris
    if event.type == MOUSEBUTTONDOWN:
        if event.button == 1:  # bouton gauche
            pos = event.pos   # (x, y)
            player.shoot(pos)

# Touches maintenues enfoncées (mouvement continu)
keys = pygame.key.get_pressed()
if keys[K_LEFT]:  player.move(-1)
if keys[K_RIGHT]: player.move( 1)
if keys[K_UP]:    player.move_y(-1)
if keys[K_DOWN]:  player.move_y( 1)
KEYDOWN / KEYUP
event.key — code de la touche
event.mod — modificateurs (Shift, Ctrl…)
event.unicode — caractère tapé
MOUSEBUTTONDOWN
event.pos — (x, y) du clic
event.button — 1=gauche, 2=milieu, 3=droit
event.button — 4/5 = molette
MOUSEMOTION
event.pos — position actuelle
event.rel — déplacement relatif
event.buttons — boutons enfoncés
Touches courantes
K_LEFT K_RIGHT K_UP K_DOWN
K_SPACE K_RETURN K_ESCAPE
K_a … K_z — lettres
K_F1 … K_F12 — fonctions
💡

Différence clé : KEYDOWN se déclenche une seule fois à l'appui (idéal pour tirer, sauter). key.get_pressed() retourne l'état continu (idéal pour le déplacement).

Dessin & couleurs

pygame.draw — formes de base
# Rectangle       surface, couleur, (x, y, w, h), [épaisseur]
pygame.draw.rect(screen, RED, (100, 50, 80, 40))
pygame.draw.rect(screen, WHITE, (100, 50, 80, 40), 2)  # contour

# Cercle          surface, couleur, centre, rayon, [épaisseur]
pygame.draw.circle(screen, (0,200,255), (400, 300), 30)
pygame.draw.circle(screen, WHITE, (400, 300), 30, 2)

# Ligne           surface, couleur, départ, arrivée, [épaisseur]
pygame.draw.line(screen, WHITE, (0, 300), (800, 300), 1)

# Polygone        surface, couleur, [(x1,y1), (x2,y2), …]
pygame.draw.polygon(screen, (255,200,0), [(400,100),(450,200),(350,200)])

# Surface avec transparence
surf = pygame.Surface((200, 100), pygame.SRCALPHA)
surf.fill((255, 0, 0, 100))  # RGBA — alpha=100
screen.blit(surf, (100, 200))

# Rect utilitaire (très pratique)
rect = pygame.Rect(100, 200, 80, 40)
rect.center   # → (140, 220)
rect.centerx  # → 140
rect.move(10, 0)  # retourne un nouveau Rect déplacé
Surfaces & blit
# blit = "block image transfer" = coller une surface sur une autre
screen.blit(image, (x, y))           # coin haut-gauche
screen.blit(image, image.get_rect(   # centré
    center=(WIDTH//2, HEIGHT//2)))

# Créer une surface de couleur unie
bg = pygame.Surface((WIDTH, HEIGHT))
bg.fill((20, 20, 40))
screen.blit(bg, (0, 0))

# Transformer une image
img = pygame.image.load("player.png").convert_alpha()
img_scaled = pygame.transform.scale(img, (64, 64))
img_rot    = pygame.transform.rotate(img, 45)  # degrés
img_flip   = pygame.transform.flip(img, True, False)  # miroir H

Toujours appeler .convert() ou .convert_alpha() après pygame.image.load() — cela convertit l'image au format de la surface d'affichage et accélère considérablement le rendu.

Sprites & images

Classe Sprite — pattern POO recommandé
class Player(pygame.sprite.Sprite):

    def __init__(self, x, y):
        super().__init__()
        # image et rect sont obligatoires pour pygame.sprite
        self.image = pygame.image.load("player.png").convert_alpha()
        self.image = pygame.transform.scale(self.image, (48, 48))
        self.rect  = self.image.get_rect(center=(x, y))

        # Vecteur de vitesse (recommandé : pygame.math.Vector2)
        self.vel = pygame.math.Vector2(0, 0)
        self.speed = 5
        self.health = 3

    def update(self):
        keys = pygame.key.get_pressed()
        self.vel.x = 0
        if keys[K_LEFT]:  self.vel.x = -self.speed
        if keys[K_RIGHT]: self.vel.x =  self.speed

        self.rect.x += self.vel.x
        # Garder dans les limites de l'écran
        self.rect.clamp_ip(screen.get_rect())

    def draw(self, surface):
        surface.blit(self.image, self.rect)
Sprite dessiné sans image (formes)
class Bullet(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        # Image créée par le code (pas de fichier)
        self.image = pygame.Surface((6, 14), pygame.SRCALPHA)
        pygame.draw.rect(self.image, (255, 220, 0), (0,0,6,14), border_radius=3)
        self.rect   = self.image.get_rect(centerx=x, bottom=y)
        self.speedy = -8

    def update(self):
        self.rect.y += self.speedy
        if self.rect.bottom < 0:
            self.kill()  # retire du groupe automatiquement
Animation par spritesheet
class AnimatedSprite(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        sheet = pygame.image.load("walk.png").convert_alpha()
        # Découper 4 frames de 48×48 sur une ligne
        self.frames = [
            sheet.subsurface((48*i, 0, 48, 48))
            for i in range(4)
        ]
        self.frame_idx = 0
        self.anim_timer = 0
        self.image = self.frames[0]
        self.rect  = self.image.get_rect()

    def animate(self, dt):
        self.anim_timer += dt
        if self.anim_timer > 120:  # changer frame toutes les 120ms
            self.anim_timer = 0
            self.frame_idx = (self.frame_idx + 1) % len(self.frames)
            self.image = self.frames[self.frame_idx]

Groupes de sprites

Créer et utiliser les groupes
# Créer les groupes
all_sprites = pygame.sprite.Group()
bullets     = pygame.sprite.Group()
enemies     = pygame.sprite.Group()

# Ajouter un sprite à un ou plusieurs groupes
player = Player(400, 500)
all_sprites.add(player)

# Créer plusieurs ennemis
for i in range(10):
    e = Enemy(i * 70 + 50, 80)
    all_sprites.add(e)
    enemies.add(e)

# Dans la boucle — update et draw en une ligne
all_sprites.update()       # appelle .update() sur tous
all_sprites.draw(screen)   # blit de tous les .image sur .rect

# Tirer une balle
def shoot():
    b = Bullet(player.rect.centerx, player.rect.top)
    all_sprites.add(b)
    bullets.add(b)

# Supprimer un sprite de tous ses groupes
enemy.kill()
📦

Un sprite peut appartenir à plusieurs groupes simultanément. On utilise généralement un groupe all_sprites pour le rendu, et des groupes spécialisés (bullets, enemies) pour les collisions et la logique.

Types de groupes
# Group — groupe standard, sprites dans un set
g = pygame.sprite.Group()

# GroupSingle — un seul sprite (joueur, curseur…)
player_group = pygame.sprite.GroupSingle(player)

# LayeredUpdates — rendu par couches (z-index)
layers = pygame.sprite.LayeredUpdates()
layers.add(background, layer=0)
layers.add(player,     layer=1)
layers.add(hud,        layer=2)

Collisions

Détection de collisions
# ── Collision entre deux sprites ──────────────

# Rect vs Rect (la plus rapide)
if pygame.sprite.collide_rect(player, enemy):
    player.take_damage()

# Cercle vs Cercle (besoin de radius sur le sprite)
if pygame.sprite.collide_circle(bullet, enemy):
    enemy.kill()

# Pixel vs Pixel (précise mais lente — utiliser avec masques)
mask_p = pygame.mask.from_surface(player.image)
mask_e = pygame.mask.from_surface(enemy.image)

# ── Collision sprite vs groupe ─────────────────

# Un sprite contre tous dans un groupe
hits = pygame.sprite.spritecollide(
    player, enemies, False  # False = ne pas tuer les ennemis
)
for hit in hits:
    player.health -= 1

# Tuer le sprite touché
pygame.sprite.spritecollide(player, enemies, True)

# Groupe vs Groupe (ex: balles vs ennemis)
hits = pygame.sprite.groupcollide(
    bullets, enemies,
    True,  # tuer la balle
    True   # tuer l'ennemi
)
score += len(hits) * 100
Collision précise avec masques
class Player(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.image.load("ship.png").convert_alpha()
        self.rect  = self.image.get_rect(center=(x, y))
        # Masque basé sur les pixels non-transparents
        self.mask  = pygame.mask.from_surface(self.image)

# Collision masque vs masque dans spritecollide
hits = pygame.sprite.spritecollide(
    player, enemies, True,
    collided=pygame.sprite.collide_mask  # ← précision pixel
)
MéthodePrécisionCoût CPU
collide_rectRectangle entourant⚡ Très rapide
collide_circleCercle englobant⚡ Rapide
collide_maskPixel par pixel🐢 Lent
🎯

Stratégie courante : utiliser collide_rect pour filtrer rapidement, puis collide_mask uniquement sur les candidats proches.

Physique & mouvements

Gravité & saut (plateforme)
class PlatformPlayer(pygame.sprite.Sprite):
    GRAVITY    = 0.5
    JUMP_SPEED = -12

    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface((32, 48))
        self.image.fill((100, 180, 255))
        self.rect  = self.image.get_rect(topleft=(x, y))
        self.vel   = pygame.math.Vector2(0, 0)
        self.on_ground = False

    def jump(self):
        if self.on_ground:
            self.vel.y = self.JUMP_SPEED
            self.on_ground = False

    def update(self, platforms):
        # Gravité
        self.vel.y += self.GRAVITY
        self.vel.y = min(self.vel.y, 15)  # vitesse max de chute

        # Déplacement horizontal
        keys = pygame.key.get_pressed()
        self.vel.x = 0
        if keys[K_LEFT]:  self.vel.x = -5
        if keys[K_RIGHT]: self.vel.x =  5

        self.rect.x += self.vel.x
        self._collide_platforms_h(platforms)

        self.rect.y += self.vel.y
        self._collide_platforms_v(platforms)

    def _collide_platforms_v(self, platforms):
        self.on_ground = False
        for plat in platforms:
            if self.rect.colliderect(plat.rect):
                if self.vel.y > 0:  # tombait vers le bas
                    self.rect.bottom = plat.rect.top
                    self.vel.y = 0
                    self.on_ground = True
vel.y += GRAVITY chaque frame
vel.y = JUMP_SPEED au saut (valeur négative)
vel.y = min(vel.y, 15) vitesse terminale
rect.y ↑ = haut de l'écran (axe Y inversé)
Vecteurs et déplacement fluide
from pygame.math import Vector2

class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.pos = Vector2(x, y)
        self.vel = Vector2(0, 0)
        self.speed = 2

    def update(self, target):
        # Se diriger vers la cible
        direction = Vector2(target.rect.center) - self.pos
        if direction.length() > 0:
            direction = direction.normalize()
        self.vel = direction * self.speed
        self.pos += self.vel
        self.rect.center = (int(self.pos.x), int(self.pos.y))

Texte & HUD

Rendu de texte
# Initialiser le moteur de polices (dans __init__)
pygame.font.init()

# Police système ou fichier .ttf
font_sm = pygame.font.SysFont("Arial", 20)
font_lg = pygame.font.SysFont("Arial", 48, bold=True)
font_px = pygame.font.Font("assets/pixel.ttf", 24)  # .ttf custom

# Rendre du texte → Surface
# render(texte, antialias, couleur, [fond])
score_surf = font_sm.render(f"Score : {score}", True, WHITE)
score_rect = score_surf.get_rect(topleft=(10, 10))
screen.blit(score_surf, score_rect)

# Centrer un texte
title = font_lg.render("GAME OVER", True, RED)
screen.blit(title, title.get_rect(center=(WIDTH//2, HEIGHT//2)))
HUD complet — classe dédiée
class HUD:
    def __init__(self):
        self.font = pygame.font.SysFont("monospace", 20, bold=True)

    def draw(self, surface, score, lives, health, level):
        # Score en haut à gauche
        self._text(surface, f"SCORE {score:06d}", WHITE, (10, 10))

        # Vies en haut à droite
        hearts = "♥ " * lives + "♡ " * (3 - lives)
        self._text(surface, hearts, RED, (0, 10), right=True)

        # Barre de vie
        pygame.draw.rect(surface, (60,60,60), (10, 35, 150, 10))
        w = int(150 * health / 100)
        color = (80,200,80) if health > 50 else (220,80,80)
        pygame.draw.rect(surface, color, (10, 35, w, 10))

        # Niveau centré en bas
        self._text(surface, f"NIVEAU {level}", (180,130,255),
                   (WIDTH//2, HEIGHT-20), center=True)

    def _text(self, surface, txt, color, pos,
               right=False, center=False):
        surf = self.font.render(txt, True, color)
        rect = surf.get_rect()
        if right:  rect.topright  = (WIDTH - 10, pos[1])
        elif center: rect.midbottom = pos
        else:      rect.topleft   = pos
        surface.blit(surf, rect)
SCORE 004200
♥ ♥ ♡
NIVEAU 3

Sons & musique

Effets sonores (Sound)
# Initialiser le mixer (dans le démarrage du jeu)
pygame.mixer.init(frequency=44100, size=-16, channels=2)

# Charger les sons (WAV ou OGG recommandé)
class SoundManager:
    def __init__(self):
        self.sounds = {
            "shoot":    pygame.mixer.Sound("assets/shoot.wav"),
            "explode":  pygame.mixer.Sound("assets/explode.wav"),
            "pickup":   pygame.mixer.Sound("assets/pickup.ogg"),
        }
        # Régler le volume (0.0 → 1.0)
        self.sounds["shoot"].set_volume(0.3)
        self.sounds["explode"].set_volume(0.7)

    def play(self, name):
        if name in self.sounds:
            self.sounds[name].play()

# Utilisation
sfx = SoundManager()
sfx.play("shoot")
sfx.play("explode")
Musique de fond (mixer.music)
# Charger et jouer (MP3 ou OGG)
pygame.mixer.music.load("assets/theme.mp3")
pygame.mixer.music.set_volume(0.5)
pygame.mixer.music.play(-1)  # -1 = boucle infinie

# Contrôles
pygame.mixer.music.pause()
pygame.mixer.music.unpause()
pygame.mixer.music.stop()
pygame.mixer.music.fadeout(2000)  # fade en 2 secondes

# Changer de musique (ex: menu → jeu)
def play_music(path, volume=0.5):
    pygame.mixer.music.stop()
    pygame.mixer.music.load(path)
    pygame.mixer.music.set_volume(volume)
    pygame.mixer.music.play(-1)

# Détecter la fin de la musique
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1)
for event in pygame.event.get():
    if event.type == pygame.USEREVENT + 1:
        play_music("assets/next_track.mp3")
🎵

Formats recommandés : .ogg pour les effets sonores (petit, libre), .mp3 ou .ogg pour la musique. Éviter les .wav non compressés pour les longues pistes — ils prennent beaucoup de mémoire.

Caméra & scrolling

Caméra centrée sur le joueur
class Camera:
    def __init__(self, world_w, world_h):
        self.offset = pygame.math.Vector2(0, 0)
        self.world_w = world_w
        self.world_h = world_h

    def update(self, target):
        # Centrer la caméra sur la cible
        self.offset.x = target.rect.centerx - WIDTH  // 2
        self.offset.y = target.rect.centery - HEIGHT // 2
        # Empêcher de sortir du monde
        self.offset.x = max(0, min(self.offset.x, self.world_w - WIDTH))
        self.offset.y = max(0, min(self.offset.y, self.world_h - HEIGHT))

    def apply(self, entity):
        # Retourne la position à l'écran
        return entity.rect.move(-self.offset)

    def draw_group(self, surface, group):
        for sprite in group:
            surface.blit(sprite.image, self.apply(sprite))

# Utilisation dans la boucle
camera = Camera(3200, 1200)  # monde 4× plus large que l'écran

# Update + Draw
camera.update(player)
camera.draw_group(screen, all_sprites)
Parallax scrolling
class ParallaxLayer:
    def __init__(self, image_path, speed_factor):
        self.image = pygame.image.load(image_path).convert()
        # Image doit être au moins 2× la largeur de l'écran
        self.rect   = self.image.get_rect()
        self.speed  = speed_factor  # 0.1 = lent (loin), 1.0 = rapide (proche)
        self.scroll = 0.0

    def update(self, camera_x):
        self.scroll = -(camera_x * self.speed) % WIDTH

    def draw(self, surface):
        x = int(self.scroll)
        surface.blit(self.image, (x, 0))
        surface.blit(self.image, (x - WIDTH, 0))  # tuile suivante

# Créer 3 couches (ciel, nuages, montagnes)
layers = [
    ParallaxLayer("bg_sky.png",       0.1),
    ParallaxLayer("bg_clouds.png",    0.3),
    ParallaxLayer("bg_mountains.png", 0.6),
]

Système de scènes

🏠
MenuScene
🎮
GameScene
💀
GameOver
⏸️
PauseScene
Classe de base Scene + SceneManager
class Scene:
    """Classe de base pour toutes les scènes."""
    def __init__(self, manager):
        self.manager = manager  # référence au gestionnaire

    def handle_events(self, events): pass
    def update(self): pass
    def draw(self, screen): pass
    def on_enter(self): pass  # appelé à l'arrivée dans la scène
    def on_exit(self):  pass  # appelé au départ de la scène


class SceneManager:
    def __init__(self):
        self.scenes  = {}
        self.current = None
        self.stack   = []   # pile pour pause/retour

    def add(self, name, scene):
        self.scenes[name] = scene

    def switch(self, name):
        # Changer de scène (remplace la courante)
        if self.current:
            self.current.on_exit()
        self.current = self.scenes[name]
        self.current.on_enter()

    def push(self, name):
        # Empiler (ex: ouvrir le menu pause sans détruire le jeu)
        if self.current:
            self.stack.append(self.current)
            self.current.on_exit()
        self.current = self.scenes[name]
        self.current.on_enter()

    def pop(self):
        # Retourner à la scène précédente
        if self.stack:
            self.current.on_exit()
            self.current = self.stack.pop()
            self.current.on_enter()
Scènes concrètes
class MenuScene(Scene):
    def on_enter(self):
        pygame.mixer.music.load("assets/menu.mp3")
        pygame.mixer.music.play(-1)
        self.font = pygame.font.SysFont("Arial", 48, bold=True)

    def handle_events(self, events):
        for e in events:
            if e.type == KEYDOWN and e.key == K_RETURN:
                self.manager.switch("game")
            if e.type == KEYDOWN and e.key == K_ESCAPE:
                pygame.quit(); exit()

    def draw(self, screen):
        screen.fill((10, 10, 30))
        title = self.font.render("SPACE SHOOTER", True, (249,115,22))
        screen.blit(title, title.get_rect(center=(WIDTH//2, 200)))


class PauseScene(Scene):
    def handle_events(self, events):
        for e in events:
            if e.type == KEYDOWN and e.key == K_p:
                self.manager.pop()  # revenir au jeu

    def draw(self, screen):
        # Superposer un voile semi-transparent
        overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 150))
        screen.blit(overlay, (0, 0))


# ── Boucle principale avec SceneManager ───────
manager = SceneManager()
manager.add("menu",  MenuScene(manager))
manager.add("game",  GameScene(manager))
manager.add("over",  GameOverScene(manager))
manager.add("pause", PauseScene(manager))
manager.switch("menu")

while running:
    events = pygame.event.get()
    for e in events:
        if e.type == QUIT: running = False
        if e.type == KEYDOWN and e.key == K_p:
            manager.push("pause")

    manager.current.handle_events(events)
    manager.current.update()
    manager.current.draw(screen)
    pygame.display.flip()
    clock.tick(FPS)

Mini-projet — Space Shooter

🚀

Ce mini-projet intègre tous les concepts vus : boucle de jeu, sprites, groupes, collisions, sons, HUD et système de scènes. Structure en 6 fichiers.

Structure du projet
space_shooter/
├── main.py          ← point d'entrée, boucle + SceneManager
├── settings.py      ← constantes (WIDTH, HEIGHT, FPS, couleurs)
├── scenes/
│   ├── __init__.py
│   ├── menu.py      ← MenuScene
│   ├── game.py      ← GameScene (logique principale)
│   └── gameover.py  ← GameOverScene
├── entities/
│   ├── __init__.py
│   ├── player.py    ← Player(Sprite)
│   ├── enemy.py     ← Enemy(Sprite)
│   └── bullet.py    ← Bullet(Sprite)
└── assets/
    ├── player.png
    ├── enemy.png
    ├── shoot.wav
    └── theme.mp3
game.py — GameScene complète
import pygame, random
from pygame.locals import *
from scenes.base import Scene
from entities.player import Player
from entities.enemy  import Enemy
from entities.bullet import Bullet
from settings import WIDTH, HEIGHT

class GameScene(Scene):

    def on_enter(self):
        self.all_sprites = pygame.sprite.Group()
        self.bullets     = pygame.sprite.Group()
        self.enemies     = pygame.sprite.Group()

        self.player = Player(WIDTH // 2, HEIGHT - 80)
        self.all_sprites.add(self.player)

        self.score   = 0
        self.wave    = 1
        self.spawn_timer = 0
        self.shoot_cooldown = 0

        self.sfx_shoot   = pygame.mixer.Sound("assets/shoot.wav")
        self.sfx_explode = pygame.mixer.Sound("assets/explode.wav")
        pygame.mixer.music.load("assets/theme.mp3")
        pygame.mixer.music.play(-1)

    def handle_events(self, events):
        for e in events:
            if e.type == KEYDOWN and e.key == K_p:
                self.manager.push("pause")

    def update(self):
        self.all_sprites.update()

        # Tir automatique maintenu
        keys = pygame.key.get_pressed()
        self.shoot_cooldown -= 1
        if keys[K_SPACE] and self.shoot_cooldown <= 0:
            b = Bullet(self.player.rect.centerx,
                       self.player.rect.top)
            self.all_sprites.add(b)
            self.bullets.add(b)
            self.sfx_shoot.play()
            self.shoot_cooldown = 12

        # Spawn d'ennemis
        self.spawn_timer += 1
        if self.spawn_timer > 60 - self.wave * 5:
            self.spawn_timer = 0
            e = Enemy(random.randint(30, WIDTH-30), -30)
            self.all_sprites.add(e)
            self.enemies.add(e)

        # Collisions balles ↔ ennemis
        hits = pygame.sprite.groupcollide(
            self.bullets, self.enemies, True, True)
        for _ in hits:
            self.score += 100
            self.sfx_explode.play()

        # Collision joueur ↔ ennemis
        if pygame.sprite.spritecollide(
                self.player, self.enemies, True):
            self.player.health -= 1
            if self.player.health <= 0:
                self.manager.switch("over")

        # Monter de vague tous les 1000 points
        self.wave = 1 + self.score // 1000

    def draw(self, screen):
        screen.fill((5, 5, 20))
        self.all_sprites.draw(screen)
        # HUD
        font = pygame.font.SysFont("monospace", 20, bold=True)
        s = font.render(f"SCORE {self.score:06d}  VAGUE {self.wave}",
                        True, (249,115,22))
        screen.blit(s, (10, 10))
        h = font.render(f"♥ {self.player.health}", True, (239,68,68))
        screen.blit(h, h.get_rect(topright=(WIDTH-10, 10)))

Cheat Sheet Pygame

🪟 Fenêtre & boucle

pygame.init()Initialiser tous les modules
display.set_mode((w,h))Créer la fenêtre
display.flip()Afficher le frame
clock.tick(fps)Limiter et mesurer le temps
event.get()Lire les événements
key.get_pressed()État continu des touches

🖼️ Sprites & groupes

Sprite.__init__Appeler super().__init__()
self.imageSurface du sprite (obligatoire)
self.rectPosition et hitbox (obligatoire)
group.add(sprite)Ajouter au groupe
group.update()Appeler update() sur tous
sprite.kill()Retirer de tous les groupes

💥 Collisions

collide_rect(a, b)Deux sprites
spritecollide(s, g, kill)Sprite vs groupe
groupcollide(g1,g2,k1,k2)Groupe vs groupe
collide_maskPrécision pixel (lent)
rect.colliderect(r)Rect vs Rect
rect.clamp_ip(r)Garder dans les limites

🎵 Sons & médias

mixer.Sound("f.wav")Charger un effet
sound.play()Jouer une fois
mixer.music.load()Charger la musique
mixer.music.play(-1)Jouer en boucle
image.load().convert_alpha()Charger une image
transform.scale(img, (w,h))Redimensionner
Rect — attributs de positionnement
# pygame.Rect(x, y, width, height)
rect = pygame.Rect(100, 200, 64, 64)

# Coins
rect.topleft   # (100, 200)  rect.topright   # (164, 200)
rect.bottomleft # (100, 264) rect.bottomright # (164, 264)

# Centres
rect.center    # (132, 232)
rect.centerx   # 132         rect.centery  # 232
rect.midtop    # (132, 200)  rect.midbottom # (132, 264)

# Dimensions
rect.width # 64  rect.height # 64  rect.size # (64, 64)

# Créer un rect centré (très utile)
image.get_rect(center=(WIDTH//2, HEIGHT//2))
image.get_rect(topleft=(10, 10))
image.get_rect(midbottom=(WIDTH//2, HEIGHT-20))