Dojo Defender : Session 5

Aujourd’hui, tu donnes à Dojo Defender un effet de profondeur avec un fond étoilé en parallaxe et des particules d’explosion. Les astéroïdes deviennent aussi destructibles : les grands se divisent en plus petits.

Ce que tu vas apprendre aujourd’hui

  • Calcul de parallaxe : plusieurs couches d’étoiles avec leur propre vitesse
  • Listes de particules : une liste de dictionnaires pour les particules
  • Effets basés sur la position : les particules apparaissent au bon endroit
  • Division récursive : grand astéroïde → 2 moyens → 2 petits → disparu

Étape 0 : Télécharge le starter

Le starter est la solution de la session 4 — Dojo Defender avec menu, trois types d’ennemis et waves.

  1. Télécharge le starter d’aujourd’hui, décompresse-le et ouvre main.py.
  2. Tu n’étais pas là ? Télécharge la solution de la session 4.
  3. Clique sur Run. Le jeu marche comme avant : vaisseau, astéroïdes, balles, drones, zigzags, chargers.

Étape 1 : 🌟 Fond étoilé en parallaxe

Un fond en parallaxe a plusieurs couches qui bougent à des vitesses différentes. Loin = lentement, près = vite. Ça donne de la profondeur.

Ajoute une liste stars = [] tout en haut du fichier, avec les autres variables globales :

stars = []

Fais une fonction init_stars() qui crée trois couches d’étoiles :

def init_stars():
    global stars
    stars = []
    layers = [
        (40, 0.3, 2, 120),   # loin : 40 étoiles, vitesse 0.3, taille 2, plus sombre
        (40, 0.7, 3, 180),   # milieu : 40 étoiles, vitesse 0.7, taille 3
        (30, 1.5, 4, 255),   # près : 30 étoiles, vitesse 1.5, taille 4, vif
    ]
    for count, speed, size, brightness in layers:
        for _ in range(count):
            stars.append({
                'x': random.randint(0, WIDTH),
                'y': random.randint(0, HEIGHT),
                'speed': speed,
                'size': size,
                'brightness': brightness,
            })

Les maths derrière : chaque étoile reçoit une vitesse y. Les étoiles lointaines (0.3 px/frame) ne bougent presque pas, les proches (1.5 px/frame) bougent plus vite. Ton cerveau interprète ça comme de la profondeur, exactement comme dans une voiture : les arbres au bord de la route défilent, les montagnes au loin restent immobiles.

Ajoute aussi la fonction d’update. Elle fait descendre les étoiles et les remet en haut quand elles passent sous l’écran :

def update_stars():
    for star in stars:
        star['y'] += star['speed']
        if star['y'] > HEIGHT + star['size']:
            star['y'] = -star['size']
            star['x'] = random.randint(0, WIDTH)

Et une fonction de dessin. Les petites étoiles, tu les dessines avec pygame.draw.rect (2×2), les plus grandes avec pygame.draw.circle :

def draw_stars(surface):
    for star in stars:
        c = (star['brightness'], star['brightness'], star['brightness'])
        if star['size'] <= 2:
            pygame.draw.rect(surface, c, (star['x'], star['y'], star['size'], star['size']))
        else:
            pygame.draw.circle(surface, c, (int(star['x']), int(star['y'])), star['size'] // 2)

Appeler : appelle init_stars() une fois (au démarrage, après toutes les fonctions), update_stars() à chaque frame (même dans le menu !), et draw_stars(screen) après screen.fill() dans TOUS les états.

Check

Tu vois trois couches d’étoiles défiler vers le bas, aussi dans le menu ? Les étoiles lointaines sont plus petites et bougent plus lentement.

Étape 2 : ✅ Basic — Système de particules

Une particule est un petit point qui bouge, s’estompe et disparaît. On utilise une liste de dictionnaires :

particles = []  # {x, y, vx, vy, life, max_life, color, size}

Fais une fonction pour faire apparaître des particules :

def spawn_particles(x, y, count, colors, speed_range, size_range, life_range):
    for _ in range(count):
        angle = random.uniform(0, 2 * math.pi)
        speed = random.uniform(speed_range[0], speed_range[1])
        particles.append({
            'x': x,
            'y': y,
            'vx': math.cos(angle) * speed,
            'vy': math.sin(angle) * speed,
            'life': random.randint(life_range[0], life_range[1]),
            'max_life': life_range[1],
            'color': random.choice(colors),
            'size': random.uniform(size_range[0], size_range[1]),
        })

Que se passe-t-il ? Chaque particule reçoit :

  • Une position (x, y)
  • Une vitesse en x et en y (vx, vy), calculée à partir d’un angle et d’une vitesse
  • Une durée de vie (life est diminué de 1 à chaque frame)
  • Une couleur et une taille

Fonction d’update (à chaque frame, déplacer chaque particule d’un pas et raccourcir sa durée de vie) :

def update_particles():
    for p in particles[:]:
        p['x'] += p['vx']
        p['y'] += p['vy']
        p['life'] -= 1
        if p['life'] <= 0:
            particles.remove(p)

Pourquoi [:] ? On modifie la liste pendant qu’on la parcourt (on supprime des particules). Sans [:], tu obtiens une erreur au runtime. La slice [:] fait une copie de la liste pour la parcourir en sécurité.

Fonction de dessin avec fondu (alpha-fade) : moins il reste de durée de vie, plus la particule est petite et sombre :

def draw_particles(surface):
    for p in particles:
        ratio = p['life'] / p['max_life']
        color = tuple(int(c * ratio) for c in p['color'])
        pygame.draw.circle(surface, color, (int(p['x']), int(p['y'])), int(p['size'] * ratio))

Le ratio est un nombre entre 1 (juste né) et 0 (presque mort). On multiplie la couleur et la taille par ce nombre, pour que la particule disparaisse lentement.

Vider la liste de particules dans le reset

Ajoute particles.clear() à la fonction reset_game(), sinon les particules mortes continuent de flotter après un restart.

Appelle update_particles() dans l’état PLAYING et draw_particles(screen) après draw_stars(screen) dans tous les états.

Check

Lance le jeu et appuie sur ESPACE. Rien de visible ne se passe encore — mais le système est prêt pour les étapes suivantes.

Étape 3 : ⭐ Stretch — Effets basés sur la position

Maintenant, on va faire apparaître des particules aux bons moments et aux bonnes positions.

Gaz d’échappement

Quand le vaisseau bouge, fais apparaître de petites particules rouge/orange sous le vaisseau. Ajoute ceci dans la section if not game_over, là où tu vérifies les flèches :

keys = pygame.key.get_pressed()
moving = False
if keys[pygame.K_LEFT] and ship.rect.centerx > 30:
    ship.rect.centerx -= 5
    moving = True
if keys[pygame.K_RIGHT] and ship.rect.centerx < WIDTH - 30:
    ship.rect.centerx += 5
    moving = True

if moving and ship.visible:
    spawn_particles(
        ship.rect.centerx, ship.rect.bottom,
        1,  # ou random.randint(1, 2)
        [(255, 100, 0), (255, 60, 0), (200, 50, 0)],
        (0.5, 1.5), (2, 4), (10, 20),
    )

Les particules apparaissent sous le vaisseau (ship.rect.bottom) et reçoivent une petite vitesse vers le haut (vy négatif à cause de l’angle).

Débris d’explosion

Quand un ennemi est touché par une balle, fais apparaître 8-12 particules dans toutes les directions. Ajoute ceci là où tu enlèves un ennemi de la liste après une collision avec une balle :

spawn_particles(
    enemy.rect.centerx, enemy.rect.centery,
    random.randint(8, 12),
    [(255, 140, 0), (255, 0, 0), (255, 255, 0), (255, 255, 255)],
    (1, 4), (2, 5), (20, 40),
)

Étincelles de mort d’ennemi

Quand un ennemi atteint le bas de l’écran, fais apparaître 3-5 étincelles blanches :

spawn_particles(
    enemy.rect.centerx, HEIGHT - 30,
    random.randint(3, 5),
    [(255, 255, 255), (200, 200, 255)],
    (1, 3), (1, 3), (15, 30),
)

Check

Bouge le vaisseau — tu vois les gaz d’échappement orange ? Tire sur un ennemi — tu vois 8-12 particules qui volent dans tous les sens ? Laisse un ennemi s’échapper — tu vois des étincelles blanches ?

Étape 4 : 🔥 Expert — Astéroïdes destructibles

Les astéroïdes peuvent maintenant être divisés de façon récursive :

  • asteroid_big.png → touché → 2× asteroid_med.png
  • asteroid_med.png → touché → 2× asteroid_small.png
  • asteroid_small.png → touché → disparu + particules

Adapte spawn_asteroid() pour que chaque astéroïde reçoive une asteroid_size :

def spawn_asteroid(size='big'):
    rock = GameObject(f'asteroid_{size}.png', random.randint(30, WIDTH - 30), -30)
    rock.speed = random.uniform(3, 5)
    rock.asteroid_size = size
    asteroids.append(rock)
    return rock

Fais une fonction split_asteroid() qui divise un astéroïde en deux plus petits :

def split_asteroid(rock):
    # D'abord : particules d'explosion
    spawn_particles(
        rock.rect.centerx, rock.rect.centery,
        random.randint(8, 12),
        [(255, 140, 0), (255, 0, 0), (255, 255, 0), (255, 255, 255)],
        (1, 4), (2, 5), (20, 40),
    )
    if rock.asteroid_size == 'big':
        for _ in range(2):
            r = spawn_asteroid('med')
            r.rect.center = rock.rect.center
            r.rect.x += random.randint(-20, 20)
            r.speed = random.uniform(2, 4)
    elif rock.asteroid_size == 'med':
        for _ in range(2):
            r = spawn_asteroid('small')
            r.rect.center = rock.rect.center
            r.rect.x += random.randint(-15, 15)
            r.speed = random.uniform(1, 3)
    asteroids.remove(rock)

Que se passe-t-il ici ?

  1. On fait apparaître des particules d’explosion à la position de l’astéroïde
  2. On vérifie la taille : ‘big’ → 2 ‘med’, ‘med’ → 2 ‘small’, ‘small’ → pas d’autre division
  3. Les nouveaux astéroïdes apparaissent près de l’ancienne position avec un léger décalage
  4. Les plus petits astéroïdes sont plus lents que les grands

Adapte la logique de collision : là où tu faisais astersoids.remove(rock) quand une balle touchait l’astéroïde, appelle maintenant split_asteroid(rock) :

for rock in asteroids[:]:
    if bullet.rect.colliderect(rock.rect):
        bullets.remove(bullet)
        explode_sound.play()
        split_asteroid(rock)
        score += 5
        break

Important : Tu joues le son (explode_sound.play()) avant split_asteroid() pour qu’il ne se joue pas deux fois par accident.

Check

Tire sur un grand astéroïde. Tu le vois se diviser en deux astéroïdes moyens ? Tire sur ceux-là — ils se divisent en deux petits. Tire sur les petits — ils disparaissent dans une belle explosion de particules.

Showcase

Montre à un coach que :

  • Fond étoilé en parallaxe avec trois vitesses
  • Système de particules avec fondu (alpha-fade)
  • Gaz d’échappement sous le vaisseau en mouvement
  • Débris d’explosion quand des ennemis et des astéroïdes sont détruits
  • Astéroïdes qui se divisent : big → med → small → disparu

À la prochaine !

« La prochaine fois : les POWER-UPS. Des boucliers, des speed-boosts, des spread-shots. Et un vrai boss final. »

À emporter chez toi

Essaie une de ces extensions à la maison :

  1. Facile : change la couleur des étoiles par couche (par exemple les étoiles lointaines bleutées, les proches jaunes).
  2. Moyen : ajoute une quatrième couche d’étoiles, encore plus rapide (vitesse 3.0), avec de grandes étoiles lumineuses.
  3. Difficile : fais changer la couleur des particules pendant leur durée de vie (du jaune au rouge puis au gris).
  4. Très difficile : ajoute un type de particule « comet » qui a une petite queue.

Télécharge Dojo Defender