Particle Effects
Creating Magic with Particles
Particle effects bring your game to life with explosions, fire, smoke, sparks, and magical effects! They're the visual spice that makes actions feel impactful and worlds feel alive. Let's master the art of particle systems! āØš
Understanding Particle Systems
š The Fireworks Analogy
Think of particle systems like fireworks:
- Emitter: The launcher that creates particles
- Particles: Individual sparks with their own life
- Properties: Color, size, velocity, lifetime
- Forces: Gravity, wind, attraction/repulsion
- Death: Particles fade or disappear after their lifetime
Interactive Particle Effects Playground
Click to create effects! Explore different particle systems!
Effect: Explosion | Particles: 0
Basic Particle System
import pygame
import random
import math
class Particle:
def __init__(self, x, y):
self.x = x
self.y = y
self.vx = random.uniform(-2, 2)
self.vy = random.uniform(-5, -1)
self.lifetime = random.uniform(0.5, 1.5)
self.size = random.randint(2, 5)
self.color = (255, random.randint(100, 255), 0)
self.gravity = 0.2
def update(self, dt):
# Physics
self.vx *= 0.99 # Air resistance
self.vy += self.gravity
# Movement
self.x += self.vx
self.y += self.vy
# Age
self.lifetime -= dt
# Fade color
fade_factor = max(0, self.lifetime)
self.color = (
int(255 * fade_factor),
int(self.color[1] * fade_factor),
0
)
return self.lifetime > 0
def draw(self, screen):
if self.lifetime > 0:
pygame.draw.circle(screen, self.color,
(int(self.x), int(self.y)),
self.size)
class ParticleSystem:
def __init__(self):
self.particles = []
def emit(self, x, y, count=10):
"""Create new particles"""
for _ in range(count):
self.particles.append(Particle(x, y))
def update(self, dt):
"""Update all particles"""
# Update particles and remove dead ones
self.particles = [p for p in self.particles if p.update(dt)]
def draw(self, screen):
"""Draw all particles"""
for particle in self.particles:
particle.draw(screen)
def clear(self):
"""Remove all particles"""
self.particles.clear()
def count(self):
"""Get particle count"""
return len(self.particles)
Advanced Particle Effects
# Configurable particle system
class ConfigurableParticle:
def __init__(self, x, y, config):
self.x = x
self.y = y
# Velocity with spread
angle = random.uniform(config.angle - config.spread,
config.angle + config.spread)
speed = random.uniform(config.speed_min, config.speed_max)
self.vx = math.cos(angle) * speed
self.vy = math.sin(angle) * speed
# Properties
self.lifetime = random.uniform(config.lifetime_min, config.lifetime_max)
self.max_lifetime = self.lifetime
self.size = random.uniform(config.size_min, config.size_max)
self.start_size = self.size
# Color
if config.color_variation:
h = config.hue + random.randint(-config.hue_variation, config.hue_variation)
s = config.saturation
v = config.value
self.color = self.hsv_to_rgb(h, s, v)
else:
self.color = config.color
# Physics
self.gravity = config.gravity
self.damping = config.damping
self.wind = config.wind
# Behavior flags
self.fade = config.fade
self.shrink = config.shrink
self.glow = config.glow
def hsv_to_rgb(self, h, s, v):
"""Convert HSV to RGB"""
import colorsys
r, g, b = colorsys.hsv_to_rgb(h/360, s, v)
return (int(r*255), int(g*255), int(b*255))
def update(self, dt):
# Apply forces
self.vy += self.gravity * dt
self.vx += self.wind * dt
# Apply damping
self.vx *= (1 - self.damping * dt)
self.vy *= (1 - self.damping * dt)
# Update position
self.x += self.vx * dt
self.y += self.vy * dt
# Update lifetime
self.lifetime -= dt
# Update size
if self.shrink:
life_ratio = self.lifetime / self.max_lifetime
self.size = self.start_size * life_ratio
return self.lifetime > 0
def draw(self, screen):
# Calculate alpha
alpha = 255
if self.fade:
alpha = int(255 * (self.lifetime / self.max_lifetime))
# Draw glow effect
if self.glow and alpha > 0:
glow_surf = pygame.Surface((self.size * 4, self.size * 4), pygame.SRCALPHA)
pygame.draw.circle(glow_surf, (*self.color, alpha // 4),
(self.size * 2, self.size * 2), self.size * 2)
screen.blit(glow_surf, (self.x - self.size * 2, self.y - self.size * 2),
special_flags=pygame.BLEND_ADD)
# Draw particle
if alpha > 0:
particle_surf = pygame.Surface((self.size * 2, self.size * 2), pygame.SRCALPHA)
pygame.draw.circle(particle_surf, (*self.color, alpha),
(self.size, self.size), self.size)
screen.blit(particle_surf, (self.x - self.size, self.y - self.size))
class ParticleConfig:
"""Configuration for particle behavior"""
def __init__(self):
# Emission
self.angle = -math.pi / 2 # Upward
self.spread = math.pi / 4 # 45 degree spread
self.speed_min = 50
self.speed_max = 150
# Lifetime
self.lifetime_min = 0.5
self.lifetime_max = 1.5
# Appearance
self.size_min = 2
self.size_max = 6
self.color = (255, 100, 0)
self.hue = 30 # Orange
self.saturation = 1.0
self.value = 1.0
self.color_variation = True
self.hue_variation = 30
# Physics
self.gravity = 100
self.damping = 0.1
self.wind = 0
# Behavior
self.fade = True
self.shrink = True
self.glow = False
# Particle emitter
class ParticleEmitter:
def __init__(self, x, y, config, system):
self.x = x
self.y = y
self.config = config
self.system = system
self.active = True
# Emission settings
self.emission_rate = 30 # particles per second
self.burst_count = 0 # 0 for continuous
self.emission_timer = 0
# Lifetime
self.lifetime = -1 # -1 for infinite
self.age = 0
def update(self, dt):
if not self.active:
return
# Update age
self.age += dt
# Check lifetime
if self.lifetime > 0 and self.age >= self.lifetime:
self.active = False
return
# Emit particles
if self.burst_count > 0:
# Burst emission
for _ in range(self.burst_count):
self.emit_particle()
self.active = False # One-shot burst
else:
# Continuous emission
self.emission_timer += dt
particles_to_emit = int(self.emission_timer * self.emission_rate)
if particles_to_emit > 0:
for _ in range(particles_to_emit):
self.emit_particle()
self.emission_timer -= particles_to_emit / self.emission_rate
def emit_particle(self):
"""Create a single particle"""
# Add position variation
x = self.x + random.uniform(-5, 5)
y = self.y + random.uniform(-5, 5)
particle = ConfigurableParticle(x, y, self.config)
self.system.particles.append(particle)
Specialized Particle Effects
# Fire effect
class FireEffect(ParticleEmitter):
def __init__(self, x, y, system):
config = ParticleConfig()
config.angle = -math.pi / 2
config.spread = math.pi / 6
config.speed_min = 30
config.speed_max = 60
config.lifetime_min = 0.3
config.lifetime_max = 0.8
config.size_min = 3
config.size_max = 8
config.hue = 30
config.hue_variation = 30
config.gravity = -50 # Fire rises
config.wind = random.uniform(-20, 20)
config.fade = True
config.shrink = True
config.glow = True
super().__init__(x, y, config, system)
self.emission_rate = 50
# Smoke effect
class SmokeEffect(ParticleEmitter):
def __init__(self, x, y, system):
config = ParticleConfig()
config.angle = -math.pi / 2
config.spread = math.pi / 4
config.speed_min = 10
config.speed_max = 30
config.lifetime_min = 1.0
config.lifetime_max = 2.0
config.size_min = 10
config.size_max = 20
config.color = (100, 100, 100)
config.color_variation = False
config.gravity = -20
config.wind = 10
config.fade = True
config.shrink = False
config.glow = False
super().__init__(x, y, config, system)
self.emission_rate = 20
# Explosion effect
class ExplosionEffect:
def __init__(self, x, y, system):
self.x = x
self.y = y
self.system = system
self.create_explosion()
def create_explosion(self):
# Main explosion particles
config = ParticleConfig()
config.spread = math.pi * 2 # Full circle
config.speed_min = 100
config.speed_max = 300
config.lifetime_min = 0.3
config.lifetime_max = 0.6
config.size_min = 3
config.size_max = 8
config.hue = 30
config.hue_variation = 60
config.gravity = 50
config.damping = 0.5
config.fade = True
config.shrink = True
config.glow = True
# Create burst
for _ in range(50):
particle = ConfigurableParticle(self.x, self.y, config)
self.system.particles.append(particle)
# Add smoke
smoke_config = ParticleConfig()
smoke_config.spread = math.pi * 2
smoke_config.speed_min = 20
smoke_config.speed_max = 50
smoke_config.lifetime_min = 1.0
smoke_config.lifetime_max = 2.0
smoke_config.size_min = 15
smoke_config.size_max = 25
smoke_config.color = (50, 50, 50)
smoke_config.color_variation = False
smoke_config.gravity = -10
smoke_config.fade = True
for _ in range(20):
particle = ConfigurableParticle(self.x, self.y, smoke_config)
self.system.particles.append(particle)
# Magic sparkles
class MagicSparkles(ParticleEmitter):
def __init__(self, x, y, system):
config = ParticleConfig()
config.spread = math.pi * 2
config.speed_min = 20
config.speed_max = 50
config.lifetime_min = 0.5
config.lifetime_max = 1.5
config.size_min = 1
config.size_max = 3
config.hue = 270 # Purple
config.hue_variation = 60
config.gravity = -20
config.fade = True
config.shrink = True
config.glow = True
super().__init__(x, y, config, system)
self.emission_rate = 30
def emit_particle(self):
"""Override to add spiral motion"""
particle = ConfigurableParticle(self.x, self.y, self.config)
# Add spiral motion
angle = self.age * 5
radius = 30
offset_x = math.cos(angle) * radius
offset_y = math.sin(angle) * radius
particle.x += offset_x
particle.y += offset_y
self.system.particles.append(particle)
Complete Particle System Demo
import pygame
import random
import math
class ParticleSystemDemo:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Particle Effects Demo")
self.clock = pygame.time.Clock()
# Particle system
self.particle_system = AdvancedParticleSystem()
# Emitters
self.emitters = []
# Demo settings
self.current_effect = 'fire'
self.show_info = True
# Create initial effects
self.setup_demo_effects()
def setup_demo_effects(self):
"""Create demonstration effects"""
# Campfire
fire = FireEffect(200, 400, self.particle_system)
self.emitters.append(fire)
# Magic fountain
magic = MagicSparkles(600, 400, self.particle_system)
self.emitters.append(magic)
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
elif event.type == pygame.MOUSEBUTTONDOWN:
x, y = pygame.mouse.get_pos()
if event.button == 1: # Left click
self.create_effect(self.current_effect, x, y)
elif event.button == 3: # Right click
self.create_effect('explosion', x, y)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_1:
self.current_effect = 'fire'
elif event.key == pygame.K_2:
self.current_effect = 'smoke'
elif event.key == pygame.K_3:
self.current_effect = 'explosion'
elif event.key == pygame.K_4:
self.current_effect = 'magic'
elif event.key == pygame.K_5:
self.current_effect = 'sparks'
elif event.key == pygame.K_c:
self.particle_system.clear()
self.emitters.clear()
elif event.key == pygame.K_i:
self.show_info = not self.show_info
return True
def create_effect(self, effect_type, x, y):
"""Create particle effect at position"""
if effect_type == 'fire':
emitter = FireEffect(x, y, self.particle_system)
emitter.lifetime = 3 # 3 seconds
self.emitters.append(emitter)
elif effect_type == 'smoke':
emitter = SmokeEffect(x, y, self.particle_system)
emitter.lifetime = 3
self.emitters.append(emitter)
elif effect_type == 'explosion':
ExplosionEffect(x, y, self.particle_system)
elif effect_type == 'magic':
emitter = MagicSparkles(x, y, self.particle_system)
emitter.lifetime = 3
self.emitters.append(emitter)
elif effect_type == 'sparks':
self.create_spark_burst(x, y)
def create_spark_burst(self, x, y):
"""Create burst of sparks"""
config = ParticleConfig()
config.spread = math.pi * 2
config.speed_min = 100
config.speed_max = 200
config.lifetime_min = 0.5
config.lifetime_max = 1.0
config.size_min = 1
config.size_max = 2
config.color = (255, 255, 100)
config.gravity = 200
config.fade = True
config.glow = True
for _ in range(30):
particle = ConfigurableParticle(x, y, config)
self.particle_system.particles.append(particle)
def update(self, dt):
# Update emitters
for emitter in self.emitters[:]:
emitter.update(dt)
if not emitter.active:
self.emitters.remove(emitter)
# Update particle system
self.particle_system.update(dt)
def draw(self):
# Clear screen
self.screen.fill((20, 20, 30))
# Draw ground
pygame.draw.rect(self.screen, (40, 40, 40), (0, 450, 800, 150))
# Draw particles
self.particle_system.draw(self.screen)
# Draw emitter positions
for emitter in self.emitters:
if emitter.active:
pygame.draw.circle(self.screen, (255, 255, 0, 50),
(int(emitter.x), int(emitter.y)), 5, 1)
# Draw UI
if self.show_info:
self.draw_ui()
def draw_ui(self):
"""Draw user interface"""
font = pygame.font.Font(None, 24)
# Effect selection
effects = ['1: Fire', '2: Smoke', '3: Explosion', '4: Magic', '5: Sparks']
y_offset = 10
for i, text in enumerate(effects):
color = (255, 255, 0) if text.split(':')[1].strip().lower() == self.current_effect else (200, 200, 200)
rendered = font.render(text, True, color)
self.screen.blit(rendered, (10, y_offset))
y_offset += 30
# Instructions
instructions = [
f"Current: {self.current_effect}",
f"Particles: {self.particle_system.count()}",
f"Emitters: {len(self.emitters)}",
"",
"Left Click: Create Effect",
"Right Click: Explosion",
"C: Clear All",
"I: Toggle Info"
]
y_offset = 10
for text in instructions:
rendered = font.render(text, True, (255, 255, 255))
self.screen.blit(rendered, (600, y_offset))
y_offset += 25
def run(self):
running = True
dt = 0
while running:
running = self.handle_events()
self.update(dt / 1000.0) # Convert to seconds
self.draw()
pygame.display.flip()
dt = self.clock.tick(60)
pygame.quit()
class AdvancedParticleSystem:
def __init__(self):
self.particles = []
self.max_particles = 5000
def update(self, dt):
"""Update all particles"""
# Update particles and remove dead ones
self.particles = [p for p in self.particles
if p.update(dt)]
# Limit particle count
if len(self.particles) > self.max_particles:
self.particles = self.particles[-self.max_particles:]
def draw(self, screen):
"""Draw all particles with blending"""
# Create surface for additive blending
particle_surf = pygame.Surface(screen.get_size())
particle_surf.fill((0, 0, 0))
# Draw particles to surface
for particle in self.particles:
particle.draw(particle_surf)
# Blend with main screen
screen.blit(particle_surf, (0, 0), special_flags=pygame.BLEND_ADD)
def clear(self):
"""Remove all particles"""
self.particles.clear()
def count(self):
"""Get particle count"""
return len(self.particles)
if __name__ == "__main__":
demo = ParticleSystemDemo()
demo.run()
Performance Optimization
ā” Particle System Optimization
- Object Pooling: Reuse particle objects instead of creating new
- LOD System: Reduce particles based on distance
- Batch Rendering: Draw similar particles together
- Spatial Partitioning: Only update visible particles
- GPU Particles: Use shaders for massive particle counts
- Texture Atlas: One texture for all particle types
- Frame Skipping: Update particles less frequently when needed
Practice Exercises
šÆ Particle Effect Challenges!
- Weather System: Create rain, snow, and fog effects
- Magic Spells: Design unique spell particle effects
- Destruction: Particle-based destruction system
- Trail System: Smooth trails behind moving objects
- Fluid Simulation: Simple water/smoke using particles
- Fireworks Show: Choreographed fireworks display
Key Takeaways
- ⨠Particles add life and feedback to games
- šÆ Configure particles for different effects
- ā” Emitters create continuous streams
- š Physics makes particles behave naturally
- šØ Blending modes create visual variety
- š Pool and limit particles for performance
- š§ Combine simple particles for complex effects
Congratulations! š
š Section Complete!
You've completed the Sprite Management section! You've learned:
- ā Loading and displaying images efficiently
- ā Creating smooth sprite animations
- ā Working with sprite sheets
- ā Organizing sprites with groups and layers
- ā Creating stunning particle effects
You now have all the tools to create visually rich, well-organized games with beautiful graphics and effects!
šļøāāļø Practice Exercise
šļøāāļø Exercise 1: Fountain of 200 ā Per-Particle Random Spawn, Bool-Return Cull, Lifetime-Fade in One Pygame Window
Objective: Build a single ~70-line pygame program with an emitter at (400, 540) on an 800Ć600 screen. Each press of SPACE spawns a fountain BURST of 200 short-lived particles ā each particle holds (x, y, vx, vy, lifetime, max_lifetime, color, size) with vx/vy drawn from an upward cone (vx ā [ā150, 150], vy ā [ā450, ā250]), lifetime ā [0.6, 1.4] seconds, color HSV-randomized in the warm range (hue ā [0, 60]), and size ā [2, 5]. Three orthogonal multi-particle simulation disciplines must be visible per frame: (a) per-particle random attribute spawn (every particle is independent at spawn ā the variation in (vx, vy, lifetime, color, size) is what makes the burst feel organic; particles are the degenerate case of chat-63 ai_flocking where N is large but rule-count is 1 and inter-entity interaction is 0); (b) boolean-return-enables-one-line-cull during update (each Particle.update(dt) ends with return self.lifetime > 0, and ParticleSystem.update rebuilds its list each tick via self.particles = [p for p in self.particles if p.update(dt)] ā a single comprehension that updates AND culls in one pass, no separate find-dead loop, no modify-while-iterating bug); (c) lifetime/max_lifetime ā alpha fade during draw (max_lifetime is captured once at spawn because lifetime -= dt mutates each tick; the ratio alpha = int(255 * (lifetime / max_lifetime)) maps from 1.0 fresh to 0.0 dying regardless of which random spawn-lifetime each particle drew ā the same shape as chat-46 platformer_camera's smooth-follow exponential decay and chat-49 polish_tweening's easing-on-t at per-particle scope). Particles render through per-particle pygame.Surface(..., pygame.SRCALPHA) so the alpha channel composites correctly. Gravity = 900 px/s² is positive (Pygame down-Y ā chat-43 game_mathematics_coordinates) so vy += GRAVITY*dt produces the parabolic fountain arc. HUD shows tick count, alive-particle count, total spawn count, and one alive particle's lifetime/max_lifetime ratio ā three orthogonal multi-particle simulation disciplines visible per frame as concrete numbers and visibly-fading particle alphas. CLOSES the effects module 0/1 ā 1/1 = 10th complete Phase-8 module joining pygame_basics + sprites + game_mathematics + physics + platformer + polish + networking + architecture + ai, advancing module-completeness 9/13 ā 10/13.
Instructions:
- Set up an 800Ć600 Pygame window at 60 FPS with a near-black background (10, 10, 20) and a dark grey ground rect at y=555.
- Define a
Particleclass with attributesx,y,vx,vy,lifetime,max_lifetime,color, andsize. Saveself.max_lifetime = self.lifetimeas a write-once spawn-time copy ā the fade ratio in step 4 depends on it. - Implement
Particle.update(dt)to (a) apply gravity viaself.vy += GRAVITY * dtwith GRAVITY=900, (b) Euler-integrate position viaself.x += self.vx * dt; self.y += self.vy * dt, (c) decrement lifetime viaself.lifetime -= dt, and (d) returnself.lifetime > 0as the boolean alive-flag. - Implement
Particle.draw(dest)to render through a per-particlepygame.Surface((size*2, size*2), pygame.SRCALPHA)withalpha = int(255 * (self.lifetime / self.max_lifetime))so the fade ratio drives the alpha channel; blit the per-particle surface onto the destination at(self.x - size, self.y - size)for centred placement. - Define a
ParticleSystemclass holdingself.particles = []. ImplementParticleSystem.update(dt)as a single line:self.particles = [p for p in self.particles if p.update(dt)]to update AND cull in one pass. - Implement
ParticleSystem.burst(x, y, n)to appendnfreshParticle(x, y)instances; have the Particle's__init__draw vx, vy, lifetime, color, and size fromrandom.uniform/random.randintcalls so each spawn is independent. - In the main loop: handle QUIT and KEYDOWN K_SPACE (call
system.burst(EMITTER_X, EMITTER_Y, 200)), callsystem.update(dt), fill the screen, draw the ground rect, callsystem.draw(screen), then render the HUD: tick count, alive-particle count, total-spawned count, and one alive particle's lifetime/max_lifetime ratio. - Press SPACE multiple times rapidly to confirm bursts compound and decay independently ā each burst's particles fade to alpha 0 over their own random spawn-lifetime, never via reset or extension; the HUD's alive-count climbs on each press and decays smoothly between presses as particles cull.
š” Hint
Three details that are easy to miss: (1) save self.max_lifetime = self.lifetime ONCE at spawn ā lifetime mutates each tick (lifetime -= dt), so by draw time the original is gone; the ratio lifetime / max_lifetime only stays meaningful if max_lifetime is the spawn-time snapshot. (2) self.particles = [p for p in self.particles if p.update(dt)] UPDATES every particle (the comprehension calls p.update(dt) on each one) AND filters out dead ones (the if filter keeps only those whose update returned True) in a single pass ā don't add a second find-dead loop. (3) per-particle fade requires a fresh pygame.Surface((s*2, s*2), pygame.SRCALPHA) per draw so the alpha channel composites correctly; calling pygame.draw.circle directly on the main screen surface ignores per-call alpha values because the screen surface is opaque RGB.
ā Example Solution
import pygame, random, colorsys
pygame.init()
SCREEN_W, SCREEN_H = 800, 600
EMITTER_X, EMITTER_Y = 400, 540
GRAVITY = 900 # px/s^2 (positive = falls in Pygame down-Y)
BURST = 200
screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
pygame.display.set_caption("Fountain of 200")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 22)
def hsv_to_rgb(h, s, v):
r, g, b = colorsys.hsv_to_rgb(h / 360.0, s, v)
return (int(r * 255), int(g * 255), int(b * 255))
class Particle:
def __init__(self, x, y):
self.x, self.y = x, y
self.vx = random.uniform(-150, 150)
self.vy = random.uniform(-450, -250)
self.lifetime = random.uniform(0.6, 1.4)
self.max_lifetime = self.lifetime # write-once spawn-time snapshot
self.color = hsv_to_rgb(random.uniform(0, 60), 1.0, 1.0)
self.size = random.randint(2, 5)
def update(self, dt):
self.vy += GRAVITY * dt # positive gravity = falls
self.x += self.vx * dt
self.y += self.vy * dt
self.lifetime -= dt
return self.lifetime > 0 # boolean alive-flag
def draw(self, dest):
ratio = self.lifetime / self.max_lifetime # 1.0 fresh, 0.0 dying
alpha = max(0, min(255, int(255 * ratio)))
s = self.size
psurf = pygame.Surface((s * 2, s * 2), pygame.SRCALPHA)
pygame.draw.circle(psurf, (*self.color, alpha), (s, s), s)
dest.blit(psurf, (self.x - s, self.y - s))
class ParticleSystem:
def __init__(self):
self.particles = []
self.spawned = 0
def burst(self, x, y, n):
for _ in range(n):
self.particles.append(Particle(x, y))
self.spawned += n
def update(self, dt):
# one-pass update + cull via list comprehension:
self.particles = [p for p in self.particles if p.update(dt)]
def draw(self, dest):
for p in self.particles:
p.draw(dest)
system = ParticleSystem()
tick = 0
running = True
while running:
dt = clock.tick(60) / 1000.0
tick += 1
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
system.burst(EMITTER_X, EMITTER_Y, BURST)
system.update(dt)
screen.fill((10, 10, 20))
pygame.draw.rect(screen, (40, 40, 40), (0, 555, SCREEN_W, 45)) # ground
system.draw(screen)
if system.particles:
p0 = system.particles[0]
sample = p0.lifetime / p0.max_lifetime
else:
sample = 0.0
hud = f"tick {tick} | alive {len(system.particles)} | spawned {system.spawned} | sample ratio {sample:.2f}"
screen.blit(font.render(hud, True, (220, 220, 220)), (10, 10))
screen.blit(font.render("SPACE = burst 200 | close window to quit", True, (180, 180, 180)), (10, 30))
pygame.display.flip()
pygame.quit()
šÆ Quick Quiz
Question 1: Each Particle.update(dt) ends with return self.lifetime > 0 after decrementing lifetime, and ParticleSystem.update calls it via self.particles = [p for p in self.particles if p.update(dt)]. What does this two-step pattern accomplish in one pass, and why is it preferred over the alternatives?
Question 2: The Particle.__init__ saves self.max_lifetime = self.lifetime ONCE at spawn, even though it never re-reads max_lifetime to extend or reset the lifetime. What role does this write-once snapshot play, and what would break without it?
Question 3: The lesson's FireEffect sets config.gravity = -50 # Fire rises while ExplosionEffect sets config.gravity = 50 for falling debris. What does the sign accomplish in the per-tick update line self.vy += self.gravity * dt, and why is it the OPPOSITE of math-class Newtonian intuition?
What's Next?
You've completed the Basic Module's Sprite Management section! With these skills, you're ready to move on to more advanced topics or start creating your own games with rich visual effects and polished graphics!