Module 16 of 20 Phase: Effects & Systems

Fire, Smoke & Sparks
A Reusable Particle System

Smoke, fire, rain, snow, explosions and magic effects all come from the same underlying idea: spawn a burst of tiny short-lived objects that move, fade, and disappear. This module builds one reusable ParticleSystem that powers every effect.

10 Lessons
This module
~2 hrs
Estimated time
📊
Intermediate
Difficulty
🐍
Object pooling
Key technique
Section 1

The Particle Class

A single particle needs just enough state to move and fade: position, velocity, a lifetime that counts down, and something visual (color/size, or a small sprite) that changes as it ages.

particle.py
import random

class Particle:
    def __init__(self, pos, vel, color, radius, lifetime):
        self.pos = pygame.Vector2(pos)
        self.vel = pygame.Vector2(vel)
        self.color = color
        self.radius = radius
        self.lifetime = lifetime
        self.age = 0

    def update(self, dt):
        self.age += dt
        self.pos += self.vel * dt
        self.vel *= 0.98    # slight drag so particles slow down over time

    def is_dead(self):
        return self.age >= self.lifetime

    def draw(self, screen):
        life_pct = 1 - (self.age / self.lifetime)
        alpha = max(0, int(255 * life_pct))
        surf = pygame.Surface((self.radius*2, self.radius*2), pygame.SRCALPHA)
        pygame.draw.circle(surf, (*self.color, alpha), (self.radius, self.radius), self.radius)
        screen.blit(surf, self.pos - pygame.Vector2(self.radius, self.radius))
Section 2

The Particle System Manager

The manager holds a list of every active particle, updates them all, removes dead ones, and offers emit() methods that spawn new bursts configured for a specific effect.

particle_system.py
class ParticleSystem:
    def __init__(self):
        self.particles = []

    def emit_explosion(self, pos, count=30):
        for _ in range(count):
            angle = random.uniform(0, 6.28)
            speed = random.uniform(100, 350)
            vel = pygame.Vector2(speed, 0).rotate_rad(angle)
            color = random.choice([(255,180,50), (255,100,30), (255,220,120)])
            self.particles.append(
                Particle(pos, vel, color, radius=random.randint(2,5), lifetime=random.uniform(0.4,0.9))
            )

    def update(self, dt):
        for p in self.particles:
            p.update(dt)
        self.particles = [p for p in self.particles if not p.is_dead()]

    def draw(self, screen):
        for p in self.particles:
            p.draw(screen)
⚠️
Performance note: Rebuilding the particles list every frame with a list comprehension (as above) is fine for hundreds of particles. For thousands, consider an object pool that reuses dead Particle instances instead of allocating new ones constantly.
Section 3

Configuring Different Effects

Every effect below reuses the exact same Particle/ParticleSystem classes — only the emit parameters (velocity spread, color, lifetime, gravity) change.

💨
Smoke
Slow upward velocity, gray colors, long lifetime (1-2s), slight random horizontal drift.
🔥
Fire
Fast upward velocity, orange/yellow/red colors, short lifetime (0.3-0.6s), shrinking radius.
🌧️
Rain
Continuous spawn at the top of the screen, fast downward velocity, thin blue-white lines instead of circles.
❄️
Snow
Slow downward velocity with gentle horizontal sine-wave drift, white color, long lifetime.
Sparks
Very fast, short-lived (0.1-0.3s), bright yellow/white, small radius — used on hit impacts.
💥
Explosion
Large burst count, wide random angle spread (0-360°), orange/red palette, medium lifetime.
Section 4

Opacity Decay Over a Particle's Lifetime

Every particle's draw() method computes life_pct = 1 - (age / lifetime) and turns it straight into an alpha value. Plotted over time, that relationship is a simple straight-line decay from fully opaque to fully transparent.

1.0 0.5 0.0 0% 50% 100% Opacity Lifetime (% elapsed)
A particle's opacity fades linearly over its lifetime

🧠 Quick Knowledge Check

1. What core piece of state lets a particle know when to disappear?
2. What mainly distinguishes "fire" particles from "smoke" particles in this system?
3. In the ParticleSystem.update() method, how are dead particles removed?
4. Why might an object pool be preferred over creating new Particle objects every frame for large effects?
5. What does self.vel *= 0.98 do in the Particle.update() method?
Finished Module 16?

Mark it complete to track your progress through Complete Pygame Mastery 2026.

🗒 Cheat Sheet 📝 Worksheet