Screen effects in code
Complete pygame implementations of screen shake, hit flash, and fade-to-black as drop-in components for any game loop.
- Implement screen shake as a decaying random camera offset
- Implement a hit flash with a semi-transparent coloured overlay
- Implement a fade-to-black and fade-from-black transition
The three effects below are packaged as small classes. Each instance is created once at program start and triggered by calling a single method. Press S for shake, F for flash, and D to start a fade-out/in cycle.
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 screen_effects.py.
All three effects
import pygame
import random
import sys
pygame.init()
W, H = 640, 480
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("Screen effects demo")
clock = pygame.time.Clock()
# ── ScreenShake ───────────────────────────────────────────────────────────────
class ScreenShake:
"""
Adds a decaying random offset to every draw call.
Usage: call shake.start(frames, magnitude) on impact.
Read shake.offset each frame and shift draws by it.
"""
def __init__(self):
self.frames = 0
self.magnitude = 0
self.offset = (0, 0)
def start(self, frames, magnitude):
# Allow a stronger/longer shake to override a weaker one
if frames > self.frames or magnitude > self.magnitude:
self.frames = frames
self.magnitude = magnitude
def update(self):
if self.frames > 0:
# Magnitude decays linearly to zero
m = self.magnitude * (self.frames / max(1, self.frames))
ox = random.randint(-int(m), int(m))
oy = random.randint(-int(m), int(m))
self.offset = (ox, oy)
self.frames -= 1
else:
self.offset = (0, 0)
# ── HitFlash ──────────────────────────────────────────────────────────────────
class HitFlash:
"""
Blits a semi-transparent coloured overlay for a fixed number of frames.
Usage: call flash.trigger(frames, colour, alpha) on a hit.
"""
def __init__(self, w, h):
self._surf = pygame.Surface((w, h))
self.frames = 0
self.colour = (255, 255, 255)
self.alpha = 180
def trigger(self, frames=3, colour=(255, 255, 255), alpha=180):
self.frames = frames
self.colour = colour
self.alpha = alpha
def draw(self, surface):
if self.frames > 0:
self._surf.fill(self.colour)
self._surf.set_alpha(self.alpha)
surface.blit(self._surf, (0, 0))
self.frames -= 1
# ── FadeOverlay ───────────────────────────────────────────────────────────────
class FadeOverlay:
"""
Fades the screen to black and optionally back.
Usage:
fade.fade_out(speed) — increase alpha to 255 at `speed` units/s
fade.fade_in(speed) — decrease alpha from 255 to 0 at `speed` units/s
Property `done` is True when the current fade is complete.
"""
def __init__(self, w, h):
self._surf = pygame.Surface((w, h))
self._surf.fill((0, 0, 0))
self._alpha = 0.0
self._speed = 0.0
self._target = 0.0
self.done = True
def fade_out(self, speed=300.0):
self._alpha = 0.0
self._speed = speed
self._target = 255.0
self.done = False
def fade_in(self, speed=300.0):
self._alpha = 255.0
self._speed = -speed
self._target = 0.0
self.done = False
def update(self, dt):
if self.done:
return
self._alpha += self._speed * dt
if self._speed > 0:
if self._alpha >= self._target:
self._alpha = self._target
self.done = True
else:
if self._alpha <= self._target:
self._alpha = self._target
self.done = True
def draw(self, surface):
a = int(max(0, min(255, self._alpha)))
if a > 0:
self._surf.set_alpha(a)
surface.blit(self._surf, (0, 0))
# ── Demo setup ────────────────────────────────────────────────────────────────
shake = ScreenShake()
flash = HitFlash(W, H)
fade = FadeOverlay(W, H)
font = pygame.font.SysFont(None, 26)
fading_out = False # track whether we're mid fade-out/in cycle
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_s:
shake.start(frames=14, magnitude=8)
if event.key == pygame.K_f:
flash.trigger(frames=4, colour=(255, 80, 80), alpha=160)
if event.key == pygame.K_d and fade.done:
fade.fade_out(speed=350)
fading_out = True
# When fade-out completes, start a fade-in
if fading_out and fade.done:
fade.fade_in(speed=300)
fading_out = False
shake.update()
fade.update(dt)
# ── Draw world (shifted by shake offset) ──────────────────────────────────
ox, oy = shake.offset
screen.fill((25, 25, 40))
# Representative game objects drawn with shake offset
pygame.draw.rect(screen, (80, 60, 40),
pygame.Rect(0 + ox, 400 + oy, W, 80))
pygame.draw.rect(screen, (100, 200, 120),
pygame.Rect(290 + ox, 360 + oy, 36, 40))
pygame.draw.circle(screen, (220, 80, 80),
(420 + ox, 370 + oy), 18)
# Effects drawn on top (flash is on the world layer; HUD is exempt)
flash.draw(screen)
fade.draw(screen)
# HUD — drawn after all effects so it stays legible
hud = font.render("S = shake F = flash D = fade", True, (200, 200, 200))
screen.blit(hud, (10, 10))
pygame.display.flip()
pygame.quit()
sys.exit()Integration pattern
Each effect object is self-contained. To add shake to any game:
- Create
shake = ScreenShake()at startup. - Call
shake.start(12, 6)wherever an impact occurs. - Call
shake.update()once per frame. - Offset all world-space
blit/drawcalls byshake.offset.
The flash and fade follow the same pattern: create once, trigger when needed,
call draw(screen) after all world drawing.
The HUD is drawn after the flash and fade effects in the demo so it remains
visible through a screen-wide white flash. In a real game you might want the
HUD to fade with the screen — draw it before fade.draw() in that case.
Where to go next
Next: lab — effects — adding particle explosions, screen shake, hit flash, and a scene-transition fade to the complete tile platformer.
Screen effects
Screen shake, hit flash, and fade-to-black are three cheap techniques that dramatically improve game feel without touching game logic.
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.