Code of the Day
AdvancedVisual Effects

Screen effects in code

Complete pygame implementations of screen shake, hit flash, and fade-to-black as drop-in components for any game loop.

Game DevAdvanced10 min read
By the end of this lesson you will be able to:
  • 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:

  1. Create shake = ScreenShake() at startup.
  2. Call shake.start(12, 6) wherever an impact occurs.
  3. Call shake.update() once per frame.
  4. Offset all world-space blit/draw calls by shake.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.

Finished reading? Mark it complete to track your progress.

On this page