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")
This applies multiple visual layers that can be configured through the settings screen:
- Scanlines
- Pixelation
- Flicker
- Glow
- 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))
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
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))
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))
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))
This creates a soft bloom/glow effect by:
- Downscaling the screen (blurring it),
- Upscaling it again,
- 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))
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)
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)
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
then start right from the command line
> planetoids
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
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.
🪐 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
💾 Installation
🔹 Install from PyPI (Recommended)
The easiest way to install Planetoids is through pip:
pip install planetoids-game
Once installed, launch the game by running:
planetoids
🔹 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 .
Then, start the game with:
planetoids
🚀 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 😄
Cheers!

Top comments (0)