DEV Community

Cover image for Simulating simple CRT and glitch effects in Pygame
Chris Greening
Chris Greening

Posted on

2

Simulating simple CRT and glitch effects in Pygame

For my retro-style arcade shooter Planetoids, I wanted to give the visuals a nostalgic kick—like you were staring at a dusty CRT screen in an '80s arcade.

I also wanted VHS-style glitch effects that make the screen feel alive—shaking, flickering, distorting—especially when something chaotic is happening in-game.

In this post, I’ll walk through how I implemented all of that using plain Pygame.


🔧 Overview: apply_crt_effect

Here's the single entry point we use every frame:

apply_crt_effect(screen, intensity="medium", pixelation="minimum")
Enter fullscreen mode Exit fullscreen mode

This applies multiple visual layers that can be configured through the settings screen:

  1. Scanlines
  2. Pixelation
  3. Flicker
  4. Glow
  5. VHS-style glitch effects (jitter, static, color separation)

Let’s break each one down.


1. Scanlines: Simulating CRT Lines

def _apply_scanlines(screen):
    width, height = screen.get_size()
    scanline_surface = pygame.Surface((width, height), pygame.SRCALPHA)

    for y in range(0, height, 4):
        pygame.draw.line(scanline_surface, (0, 0, 0, 60), (0, y), (width, y))

    screen.blit(scanline_surface, (0, 0))
Enter fullscreen mode Exit fullscreen mode

This draws semi-transparent horizontal black lines across the screen, spaced every 4 pixels, simulating the horizontal scanlines seen on CRT monitors.

  • Want more intense lines? Decrease the spacing or increase alpha.
  • It adds an immediate analog vibe and subtly breaks up the digital sharpness.

2. Pixelation: Deliberate Resolution Downgrade

Image description

def _apply_pixelation(screen, pixelation):
    pixelation = {"minimum": 2, "medium": 4, "maximum": 6}.get(pixelation, 2)
    width, height = screen.get_size()
    small_surf = pygame.transform.scale(screen, (width // pixelation, height // pixelation))
    screen.blit(pygame.transform.scale(small_surf, (width, height)), (0, 0))
Enter fullscreen mode Exit fullscreen mode

This temporarily shrinks the screen resolution and scales it back up, creating a pixelated, low-res effect.

  • We use this after rendering everything else so the whole frame gets affected.
  • Makes the game feel more gritty and less digital-clean.

3. Flicker: A Soft Glow Pulse

def _apply_flicker(screen):
    if random.randint(0, 20) == 0:
        flicker_surface = pygame.Surface(screen.get_size(), pygame.SRCALPHA)
        flicker_surface.fill((255, 255, 255, 5))
        screen.blit(flicker_surface, (0, 0))
Enter fullscreen mode Exit fullscreen mode

Every so often (about 5% of frames), a subtle white overlay is applied.

  • This simulates the fluctuating brightness of a CRT bulb.
  • Super subtle, but it makes the screen feel alive, especially when combined with glow.

4. Glow: Simulated Light Bleed

def _apply_glow(screen):
    width, height = screen.get_size()
    glow_surf = pygame.transform.smoothscale(screen, (width // 2, height // 2))
    glow_surf = pygame.transform.smoothscale(glow_surf, (width, height))
    glow_surf.set_alpha(100)
    screen.blit(glow_surf, (0, 0))
Enter fullscreen mode Exit fullscreen mode

This creates a soft bloom/glow effect by:

  1. Downscaling the screen (blurring it),
  2. Upscaling it again,
  3. Layering it back on top.

It's an easy way to fake blur and create that soft glow old CRTs had when they burned too bright.


5. VHS Glitch: The Controlled Chaos

This section ties everything together by adding movement, noise, and distortion.

5.1 Horizontal Slice Glitches

def _add_glitch_effect(height, width, glitch_surface, intensity):
    shift_amount = {"minimum": 10, "medium": 20, "maximum": 40}.get(intensity, 20)
    if random.random() < 0.1:
        y_start = random.randint(0, height - 20)
        slice_height = random.randint(5, 20)
        offset = random.randint(-shift_amount, shift_amount)

        slice_area = pygame.Rect(0, y_start, width, slice_height)
        slice_copy = glitch_surface.subsurface(slice_area).copy()
        glitch_surface.blit(slice_copy, (offset, y_start))
Enter fullscreen mode Exit fullscreen mode

Random slices of the screen are offset left or right—mimicking VHS tracking issues or old screen flickers.

  • Triggered randomly per frame.
  • Intensity controls how far the lines can jump.

5.2 RGB Color Separation

def _add_color_separation(screen, glitch_surface, intensity):
    color_shift = {"minimum": 2, "medium": 6, "maximum": 10}.get(intensity, 4)
    if random.random() < 0.05:
        for i in range(3):
            x_offset = random.randint(-color_shift, color_shift)
            y_offset = random.randint(-color_shift, color_shift)
            color_shift_surface = glitch_surface.copy()
            color_shift_surface.fill((0, 0, 0))
            color_shift_surface.blit(glitch_surface, (x_offset, y_offset))
            screen.blit(color_shift_surface, (0, 0), special_flags=pygame.BLEND_ADD)
Enter fullscreen mode Exit fullscreen mode

This simulates the separation of red, green, and blue channels for glitchy VHS color bleed.

  • Only triggers occasionally.
  • Intensity controls how far apart the color layers get.

5.3 Rolling Static

def _add_rolling_static(screen, height, width, intensity):
    static_chance = {"minimum": 0.1, "medium": 0.3, "maximum": 0.8}.get(intensity, 0.2)
    static_surface = pygame.Surface((width, height), pygame.SRCALPHA)

    for y in range(0, height, 8):
        if random.random() < static_chance:
            pygame.draw.line(static_surface, (255, 255, 255, random.randint(30, 80)), (0, y), (width, y))

    screen.blit(static_surface, (0, 0), special_flags=pygame.BLEND_ADD)
Enter fullscreen mode Exit fullscreen mode

A final touch: horizontal white noise lines that appear and disappear in random rows.

  • Like old static interference rolling up the screen.
  • Controlled by intensity, which raises or lowers the frequency.

🧠 Final Thoughts

All of this runs in real time using Pygame’s surface tools.

None of it requires shaders, OpenGL, or special GPU code and is a fairly lightweight approach for integrating into 2D games.

These effects make Planetoids feel like an experience—not just a game. When the screen distorts after a power-up or flickers from a near-death explosion, you don’t just see it you feel it.


🚀 Try It Out

You can install Planetoids via pip:

pip install planetoids-game
Enter fullscreen mode Exit fullscreen mode

then start right from the command line

> planetoids

GitHub logo chris-greening / planetoids

Planetoids is a fast-paced, retro-inspired arcade space shooter built in Python with Pygame. Dodge asteroids and experience a vintage arcade feel with CRT effects, glitch animations, and pixel-perfect scaling.

Planetoids: A Retro-Inspired Space Shooter

GIF of Planetoids gameplay showing asteroids, a spaceship, and CRT visual effects

What is it?

Planetoids is a fast-paced, retro-inspired arcade space shooter built in Python with Pygame.
Dodge asteroids and experience a vintage arcade feel with CRT effects, glitch animations, and pixel-perfect scaling.

Issues License PyPI Downloads

"Buy Me A Coffee"


🪐 Key Features

  • Fast-paced asteroid destruction
  • Smooth FPS-independent physics
  • Retro CRT visual effects & glitch animations
  • Classic arcade gameplay mechanics
  • Power-ups and increasing difficulty
  • Pixel-perfect scaling for all screen sizes

CRT visual effect applied to gameplay


💾 Installation

🔹 Install from PyPI (Recommended)

The easiest way to install Planetoids is through pip:

pip install planetoids-game
Enter fullscreen mode Exit fullscreen mode

Once installed, launch the game by running:

planetoids
Enter fullscreen mode Exit fullscreen mode

🔹 Install from Source

If you want the latest development version, you can install directly from GitHub:

git clone https://github.com/chris-greening/planetoids-game.git
cd planetoids-game
pip install -e .
Enter fullscreen mode Exit fullscreen mode

Then, start the game with:

planetoids
Enter fullscreen mode Exit fullscreen mode

🚀 Running the Game

Once installed, you can start Planetoids in one of the following ways:

🔹 Run from Source

If…




Follow along for more devlogs, insights, and behind-the-scenes experiments.

Conclusion

Thanks so much for reading and if you liked my content, be sure to check out some of my other work or connect with me on social media or my personal website 😄

Chris Greening - Software Developer

Hey! My name's Chris Greening and I'm a software developer from the New York metro area with a diverse range of engineering experience - beam me a message and let's build something great!

favicon christophergreening.com

Cheers!


Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay