Lab: Visual effects
Add particle explosions on enemy death, screen shake on player hit, hit flash on taking damage, and a fade transition to the complete game.
- Trigger a particle burst when an enemy dies
- Start screen shake and a hit flash whenever the player takes damage
- Apply a fade-to-black / fade-from-black on scene transitions
This lab assumes you have the tile platformer + AI enemy from the previous modules. You will wire all four visual effect systems into that game. If you are working fresh, the stub code below gives you enough scaffolding to practise the integration pattern.
Pyodide (the in-browser Python runner) does not support pygame's display
system. Read through this code carefully in the browser, then run it locally:
pip install pygame followed by python lab_effects.py.
Step 1 — Import and instantiate the effect systems
At the top of your game file, after pygame is initialised, create one instance
of each effect class (copy the three classes from the screen-effects-code
lesson):
from dataclasses import dataclass
import random, math
# Paste ScreenShake, HitFlash, FadeOverlay, Particle, and emit_burst here
# (or import them from a shared effects.py module)
shake = ScreenShake()
flash = HitFlash(W, H)
fade = FadeOverlay(W, H)
particles = []Creating one instance of each at module level keeps the main loop clean and avoids re-allocating surfaces every frame.
Step 2 — Particle explosion on enemy death
In your Enemy.update (or wherever you remove the enemy), call emit_burst
and extend the global particle list:
# Inside the main loop, after updating enemies:
alive_enemies = []
for enemy in enemies:
enemy.update(dt, player.pos)
if enemy.hp <= 0:
particles.extend(emit_burst(enemy.pos.x, enemy.pos.y, count=35))
shake.start(frames=10, magnitude=6)
else:
alive_enemies.append(enemy)
enemies = alive_enemiesThe shake call here is optional but reinforces the impact of killing an enemy. A modest value (10 frames, magnitude 6) avoids being obnoxious.
Step 3 — Screen shake and flash on player damage
When the player is hit, trigger both effects simultaneously:
def player_take_damage(player, amount):
player.hp -= amount
shake.start(frames=12, magnitude=7)
flash.trigger(frames=3, colour=(255, 60, 60), alpha=150)A red flash for damage is conventional and immediately legible. White is used for powerful hits or boss impacts where you want more contrast.
Step 4 — Fade transition between scenes
Use FadeOverlay when transitioning from the game scene to a game-over screen
or the next level:
# In the game loop, when the win/loss condition is met:
if player.hp <= 0 and not transitioning:
fade.fade_out(speed=380)
transitioning = True
# After the fade-out completes, switch scenes:
if transitioning and fade.done and not fade_in_started:
current_scene = GameOverScene()
fade.fade_in(speed=300)
fade_in_started = TrueThe two-step approach (fade out → swap scene → fade in) prevents the player from seeing a single frame of the new scene before the fade completes.
Step 5 — Putting it all together
import pygame
import sys, random, math
from dataclasses import dataclass
pygame.init()
W, H = 640, 480
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("Effects lab")
clock = pygame.time.Clock()
Vector2 = pygame.math.Vector2
# ── Paste effect classes here (ScreenShake, HitFlash, FadeOverlay) ────────────
# ── Paste Particle dataclass and emit_burst here ──────────────────────────────
shake = ScreenShake()
flash = HitFlash(W, H)
fade = FadeOverlay(W, H)
particles = []
# Minimal stand-in for player and enemy
player_pos = Vector2(100, 380)
player_hp = 100
enemy_pos = Vector2(400, 380)
enemy_hp = 60
font = pygame.font.SysFont(None, 26)
transitioning = False
fade_in_done = False
fade.fade_in(speed=400) # open the scene with a fade-in
def player_take_damage(amount):
global player_hp
player_hp = max(0, player_hp - amount)
shake.start(12, 7)
flash.trigger(3, (255, 60, 60), 150)
def kill_enemy():
global enemy_hp
particles.extend(emit_burst(enemy_pos.x, enemy_pos.y, count=35))
shake.start(10, 6)
enemy_hp = 0
running = True
while running:
dt = min(clock.tick(60) / 1000.0, 0.05)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_h: player_take_damage(20)
if event.key == pygame.K_k: kill_enemy()
if event.key == pygame.K_t and not transitioning:
fade.fade_out(380)
transitioning = True
# Trigger fade-in after fade-out completes
if transitioning and fade.done and not fade_in_done:
fade.fade_in(300)
fade_in_done = True
shake.update()
fade.update(dt)
# Update and cull particles
particles[:] = [p for p in particles if p.lifetime > 0]
for p in particles:
p.update(dt)
# ── Draw (world offset by shake) ──────────────────────────────────────────
ox, oy = shake.offset
screen.fill((20, 20, 35))
# Ground
pygame.draw.rect(screen, (80, 60, 40),
pygame.Rect(0 + ox, 410 + oy, W, 70))
# Player
pygame.draw.rect(screen, (100, 200, 120),
pygame.Rect(int(player_pos.x)+ox, int(player_pos.y)+oy, 32, 40))
# Enemy (if alive)
if enemy_hp > 0:
pygame.draw.rect(screen, (200, 60, 60),
pygame.Rect(int(enemy_pos.x)+ox, int(enemy_pos.y)+oy, 32, 32))
# Particles (drawn in world space — include shake offset)
for p in particles:
frac = p.life_frac()
r = max(1, int(p.radius * frac))
colour = tuple(int(c * frac) for c in p.colour)
pygame.draw.circle(screen, colour,
(int(p.pos.x)+ox, int(p.pos.y)+oy), r)
# Flash and fade on top of world
flash.draw(screen)
fade.draw(screen)
# HUD (unaffected by shake — drawn after all effects except fade)
hud = font.render(
f"HP:{player_hp} Enemy HP:{enemy_hp} H=damage K=kill T=transition",
True, (200, 200, 200))
screen.blit(hud, (10, 10))
pygame.display.flip()
pygame.quit()
sys.exit()What to try next
- Apply the shake offset only to world-space objects (player, enemies, tiles) and leave the HUD at its original position — observe how this clarifies the separation between game world and interface.
- Add a smoke particle system that continuously trails the enemy as it moves.
- Implement a "level clear" sequence: trigger a fade-out, load the next level, fade in.
- Experiment with different
emit_burstparameters — fewer, faster particles for a sharp crack; many slow particles for a billowing explosion.
Screen effects in code
Complete pygame implementations of screen shake, hit flash, and fade-to-black as drop-in components for any game loop.
The observer pattern
Publisher/subscriber decouples systems so an achievement engine, a sound manager, and a UI can all react to game events without the player or enemy knowing they exist.