Code of the Day
AdvancedVisual Effects

Screen effects

Screen shake, hit flash, and fade-to-black are three cheap techniques that dramatically improve game feel without touching game logic.

Game DevAdvanced5 min read
By the end of this lesson you will be able to:
  • Explain screen shake as a random camera offset applied for N frames
  • Describe a hit flash as a semi-transparent white overlay blitted for 2-3 frames
  • Implement a fade-in/fade-out by gradually adjusting the alpha of a black fill surface

Three screen-level effects account for a large proportion of the "game feel" in a polished 2D game. None of them require changes to game logic: they are applied in the render pass as a post-processing step.

Screen shake

When an impact occurs — the player lands a heavy blow, an explosion goes off, a boss slams the floor — a brief camera shake signals the weight of the event.

The implementation: maintain a shake_frames counter and a shake_magnitude value. While shake_frames > 0, add a random offset to the camera's draw position each frame:

if shake_frames > 0:
    offset_x = random.randint(-shake_magnitude, shake_magnitude)
    offset_y = random.randint(-shake_magnitude, shake_magnitude)
    shake_frames -= 1
else:
    offset_x = offset_y = 0

All world drawing is translated by (offset_x, offset_y). The HUD is drawn at its normal position so it does not shake. A duration of 8–12 frames and a magnitude of 4–8 px covers most impacts. Large explosions might use 16 frames and 12 px.

Decay is optional but polished: instead of a constant magnitude, multiply it by the fraction of frames remaining — the shake is strongest on impact and tapers to zero.

Hit flash

When the player takes damage or an enemy is struck, a one or two-frame white overlay communicates the hit instantly, even before any animation plays.

The technique: blit a white surface with partial alpha (try 160–200 out of 255) over the entire screen for 2–3 frames:

if flash_frames > 0:
    flash_surface.fill((255, 255, 255))
    flash_surface.set_alpha(180)
    screen.blit(flash_surface, (0, 0))
    flash_frames -= 1

The flash_surface is a plain pygame.Surface((W, H)) created once at startup. Setting alpha each frame is cheap. The same technique works for a red flash (damage taken) or a yellow flash (power-up collected) by changing the fill colour.

Fade-to-black

Scene transitions feel abrupt without a fade. Fading to black and back in requires a dedicated black surface whose alpha ramps up or down:

fade_alpha  += fade_speed * dt   # fade_speed in alpha units per second (e.g. 400)
fade_alpha   = min(255, fade_alpha)
fade_surface.fill((0, 0, 0))
fade_surface.set_alpha(int(fade_alpha))
screen.blit(fade_surface, (0, 0))

At fade_alpha == 0 the surface is invisible; at 255 the screen is completely black. A fade-out runs the alpha from 0 → 255, then triggers the scene change; a fade-in runs 255 → 0 at the start of the new scene.

All three effects operate on surfaces that are pre-allocated at startup. Avoid creating surfaces inside the game loop — surface allocation is relatively expensive and does not belong on the hot path.

Combining effects

On a powerful hit, trigger all three simultaneously:

  • shake_frames = 12, shake_magnitude = 8
  • flash_frames = 3
  • begin a fade_out if the hit is lethal

Stacking these effects reinforces the same event from multiple sensory channels (motion, brightness, darkness) and makes the moment feel weighty without requiring new assets or animations.

Where to go next

Next: screen effects in code — complete implementations of all three effects as standalone functions that slot into any pygame game loop.

Finished reading? Mark it complete to track your progress.

On this page