Dojo Defender: Sessie 5

Vandaag geef je Dojo Defender een diepte-effect met een parallax sterrenachtergrond en explosie-deeltjes. Asteroïden worden ook vernietigbaar: grote asteroïden splitsen in kleinere.

Wat je vandaag leert

  • Parallax rekenen: meerdere lagen sterren met eigen snelheid
  • Deeltjeslijsten: een lijst van dictionaries voor particles
  • Positie-gebaseerde effecten: particles spawnen op de juiste plek
  • Recursief splitsen: grote asteroïde → 2 medium → 2 klein → weg

Stap 0: Download de starter

De starter is de oplossing van sessie 4 — Dojo Defender met menu, drie vijandtypes en waves.

  1. Download de starter voor vandaag, pak uit en open main.py.
  2. Was je er niet bij? Download de oplossing van sessie 4.
  3. Klik op Run. Het spel werkt zoals voorheen: schip, asteroïden, kogels, drones, zigzags, chargers.

Stap 1: 🌟 Parallax sterrenachtergrond

Een parallax achtergrond heeft meerdere lagen die met verschillende snelheden bewegen. Ver weg = langzaam, dichtbij = snel. Dat geeft diepte.

Voeg een lijst stars = [] toe bovenaan het bestand, bij de andere globale variabelen:

stars = []

Maak een init_stars() functie die drie lagen sterren aanmaakt:

def init_stars():
    global stars
    stars = []
    layers = [
        (40, 0.3, 2, 120),   # ver weg: 40 sterren, speed 0.3, grootte 2, donkerder
        (40, 0.7, 3, 180),   # middel:  40 sterren, speed 0.7, grootte 3
        (30, 1.5, 4, 255),   # dichtbij: 30 sterren, speed 1.5, grootte 4, fel
    ]
    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,
            })

Wiskunde erachter: elke ster krijgt een y-snelheid. Verre sterren (0.3 px/frame) bewegen bijna niet, nabije sterren (1.5 px/frame) bewegen sneller. Je brein interpreteert dat als diepte, precies zoals in een auto: bomen naast de weg vliegen voorbij, bergen ver weg blijven stil.

Voeg ook de update-functie toe. Die laat sterren zakken en zet ze terug naar boven als ze onder het scherm zijn:

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)

En een teken-functie. Kleine sterren teken je met pygame.draw.rect (2×2), grotere met 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)

Aanroepen: Roep init_stars() één keer aan (bij het opstarten, ná alle functies), update_stars() elke frame (zelfs in het menu!), en draw_stars(screen) na screen.fill() in ALLE states.

Check

Zie je drie lagen sterren naar beneden scrollen, ook in het menu? De verre sterren zijn kleiner en bewegen langzamer.

Stap 2: ✅ Basic — Particle systeem

Een particle is een klein deeltje dat beweegt, vervaagt en verdwijnt. We gebruiken een lijst van dictionaries:

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

Maak een functie om particles te spawnen:

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]),
        })

Wat gebeurt er? Elke particle krijgt:

  • Een positie (x, y)
  • Een snelheid in x en y (vx, vy), berekend uit een hoek en snelheid
  • Een levensduur (life wordt elke frame met 1 verlaagd)
  • Een kleur en grootte

Update-functie (elke frame elke particle 1 stap verplaatsen en de levensduur verkorten):

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)

Waarom [:]? We passen de lijst aan tijdens het loopen (we verwijderen particles). Zonder [:] krijg je een runtime error. De slice [:] maakt een kopie van de lijst om veilig over te loopen.

Teken-functie met vervaging (alpha-fade): hoe minder levensduur, hoe kleiner en donkerder:

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))

De ratio is een getal tussen 1 (net geboren) en 0 (bijna dood). We vermenigvuldigen de kleur en grootte ermee, zodat de particle langzaam verdwijnt.

Particle-lijst wissen in reset

Voeg particles.clear() toe aan de reset_game() functie, anders blijven dode particles na een restart zweven.

Roep update_particles() aan in de PLAYING state en draw_particles(screen) na draw_stars(screen) in alle states.

Check

Start het spel en druk op SPACE. Er gebeurt nog niets zichtbaars — maar het systeem staat klaar voor de volgende stappen.

Stap 3: ⭐ Stretch — Positie-gebaseerde effecten

Nu gaan we particles spawnen op de juiste momenten en posities.

Uitlaatgassen

Als het schip beweegt, spawn kleine rode/oranje particles onder het schip. Voeg dit toe in de if not game_over sectie, waar je de pijltjestoetsen checked:

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,  # of random.randint(1, 2)
        [(255, 100, 0), (255, 60, 0), (200, 50, 0)],
        (0.5, 1.5), (2, 4), (10, 20),
    )

De particles spawnen onder het schip (ship.rect.bottom) en krijgen een kleine snelheid omhoog (negatieve vy door de hoek).

Explosie puin

Als een vijand wordt geraakt door een kogel, spawn 8-12 deeltjes in alle richtingen. Voeg dit toe waar je een enemy uit de lijst haalt na een bullet-collision:

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),
)

Vijand-sterfsparkels

Als een vijand de onderkant van het scherm bereikt, spawn 3-5 witte vonkjes:

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

Check

Beweeg het schip — zie je oranje uitlaatgassen? Schiet een vijand — zie je 8-12 rondvliegende deeltjes? Laat een vijand ontsnappen — zie je witte vonkjes?

Stap 4: 🔥 Expert — Vernietigbare asteroïden

Asteroïden zijn nu recursief splitsbaar:

  • asteroid_big.png → wordt geraakt → 2× asteroid_med.png
  • asteroid_med.png → wordt geraakt → 2× asteroid_small.png
  • asteroid_small.png → wordt geraakt → weg + particles

Pas spawn_asteroid() aan zodat elke asteroïde een asteroid_size krijgt:

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

Maak een split_asteroid() functie die een asteroïde in twee kleinere splitst:

def split_asteroid(rock):
    # Eerst: exploderende particles
    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)

Wat gebeurt hier?

  1. We spawnen explosie-particles op de positie van de asteroïde
  2. We checken de grootte: ‘big’ → 2 ‘med’, ‘med’ → 2 ‘small’, ‘small’ → geen verdere splitsing
  3. Nieuwe asteroïden spawnen vlakbij de oude positie met een lichte offset
  4. De kleinere asteroïden zijn langzamer dan de grotere

Pas de collision-logic aan: waar je astersoids.remove(rock) deed bij een kogelraak, roep nu split_asteroid(rock) aan:

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

Belangrijk: Het geluid (explode_sound.play()) speel je af vóór split_asteroid() zodat het niet per ongeluk twee keer afspeelt.

Check

Schiet een grote asteroïde. Zie je hem splitsen in twee medium asteroïden? Schiet die — ze splitsen in twee kleine. Schiet de kleine — die verdwijnen met een mooie particle-explosie.

Showcase

Laat aan een coach zien dat:

  • Parallax sterrenachtergrond met drie snelheden
  • Particle systeem met vervaging (alpha-fade)
  • Uitlaatgassen onder het bewegende schip
  • Explosie-puin bij vernietigde vijanden en asteroïden
  • Asteroïden splitsen: big → med → small → weg

Tot de volgende keer!

“Volgende keer: POWER-UPS. Schilden, speed-boosts, spread-shots. En een échte eindbaas.”

Neem mee naar huis

Probeer thuis één van deze uitbreidingen:

  1. Makkelijk: verander de kleur van de sterren per laag (bijv. verre sterren blauwachtig, nabije geel).
  2. Middel: voeg een vierde laag sterren toe, nog sneller (speed 3.0), met grote heldere sterren.
  3. Lastig: laat particle-kleur veranderen tijdens de levensduur (van geel naar rood naar grijs).
  4. Erg lastig: voeg een “comet” particle type toe dat een klein staartje heeft.

Download Dojo Defender