In this guide you’ll build the Pac-Man chase animation shown on the 16×16 RGB LED matrix panel product page: Pac-Man and the four classic ghosts running across a long horizontal strip of 8 panels chained end-to-end (128 × 16 pixels), driven by a Raspberry Pi.
It’s a great first project for the panels because it covers everything you need for bigger displays later — chaining, managing power, the driver library, and sprite-based animation in Python.
What you’ll build
A 8-panel horizontal strip — 128 pixels wide, 16 pixels tall — showing Pac-Man chasing the four ghosts (Blinky, Pinky, Inky and Clyde) across the display. Animation runs at around 25 FPS and loops forever.
Bill of materials
- 8 × OzzMaker 16×16 RGB LED Matrix Panel (each ships with one FFC cable)
- 1 × OzzMaker RGB LED Matrix Connector for Raspberry Pi
- 1 × Raspberry Pi 4, Pi 3, Pi Zero 2 W, or Pi Zero
- 1 × 5 V 3 A power supply
- A microSD card with Raspberry Pi OS
Assemble hardware
1. Fit the connector to the Pi
Power off the Pi. Sit the OzzMaker LED connector down onto the 40-pin GPIO header so it covers all 40 pins. The connector breaks out the unused GPIO and includes a QWIIC socket for I²C.
2. Chain the panels
Each panel has an IN and OUT FFC port. The arrow on the back of the PCB shows the direction data flows. Plug an FFC cable from the connector on the Pi into the IN port of panel 1, then from OUT of panel 1 to IN of panel 2, and so on, all the way to panel 8.
A common mistake: getting one of the FFC cables in backwards or 180° rotated. The contacts on the cable should always face the same side of the connector latch. If a panel half-way down the chain shows garbage, that’s almost always the cause.
3. Power
The FFC cable carries 5 V and GND alongside the data lines and you should be able to run this animation without needing a separate power supply for some of the LED panels as most of the LEDs are off during the animation. If you see flickering or garbage towards the end of the chain, this would most likely mean the power supply powering your Pi and LED panels isn’t reliable or powerful enough. From the panel before you see the problem, feed 5 V and GND directly to the 5V and GND pads on the back of panel 7. Run those wires from the same supply that’s powering the Pi (or a second 5 V supply with a common ground — never two supplies without a common ground).
If you’re using a second supply, cut the trace between the green-highlighted pads on the back of panel where you have connected the second power supply. See the LED matrix overview guide for the pad locations.
4. Soldering panels together
Before permanently soldering the panels together for a rigid display, you should run a test to make sure everything is connect properly. Once it is confirmed working, you can now begin soldering. First place some solder on the pads near there edge of the panels, then aligned them together and place more solder on the two pads until the solder flows across both pads, as show in the image below.
Software setup
1. Install libraries
Follow this guide to get hzeller’s library installed which will be used to run the RGB LED panels.
2. Download code
You can download the pacman demo from our github repositry
cd into the pacman folder and run the script
pi@raspberrypi ~ $ cd pacman
pi@raspberrypi ~ $ sudo python pacman.py
Below is the main python script, which is well document and easy to follow.
#!/usr/bin/env python
from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics
#from samplebase import SampleBase
import time
import math
import colorsys
from PIL import Image
options = RGBMatrixOptions()
options.rows = 16
options.cols = 16
options.chain_length = 10
options.gpio_slowdown = 2
options.brightness = 30
matrix = RGBMatrix(options = options)
def clearMatrix():
for x in range(matrix.width):
for y in range( matrix.height):
double_buffer.SetPixel(x, y, 0,0,0)
from PIL import Image
import time
# Define constants
MATRIX_WIDTH = 160
MATRIX_HEIGHT = 16
SPRITE_WIDTH = 16 # Assuming each sprite is 16 pixels wide
PACMAN_WIDTH = 16 # Assuming Pacman is also 16 pixels wide
GAP = 8 # Gap between ghosts
INITIAL_PACMAN_GAP = 80 # Initial gap between Pacman and the first ghost
FINAL_PACMAN_GAP = -SPRITE_WIDTH # Final gap where ghosts overlap Pacman
ANIMATION_DELAY = 0.03 # Delay between frames in seconds
PACMAN_DIES_LOCATION = 130
PACMAN_DIES_DELAY = 0.07 # Delay between frames in the Pacman death animation
# Load sprite images
ghostRed1 = Image.open('ghostRed1.ppm').convert('RGB')
ghostRed2 = Image.open('ghostRed2.ppm').convert('RGB')
ghostPink1 = Image.open('ghostPink1.ppm').convert('RGB')
ghostPink2 = Image.open('ghostPink2.ppm').convert('RGB')
ghostLightBlue1 = Image.open('ghostLightBlue1.ppm').convert('RGB')
ghostLightBlue2 = Image.open('ghostLightBlue2.ppm').convert('RGB')
ghostOrange1 = Image.open('ghostOrange1.ppm').convert('RGB')
ghostOrange2 = Image.open('ghostOrange2.ppm').convert('RGB')
# Load Pacman images
pacman1 = Image.open('pacman1.ppm').convert('RGB')
pacman2 = Image.open('pacman2.ppm').convert('RGB')
pacman3 = Image.open('pacman3.ppm').convert('RGB')
# Load PacmanDies images
pacmanDies = [
Image.open(f'pacmanDies{i+1}.ppm').convert('RGB') for i in range(11)
]
# Initialize double buffer and matrix
double_buffer = matrix.CreateFrameCanvas()
# List of sprites, their positions, and individual frame counters
sprites = [
{'images': [pacman1, pacman2, pacman3], 'x': -PACMAN_WIDTH, 'frame': 0, 'switch_rate': 4},
{'images': [ghostRed1, ghostRed2], 'x': -PACMAN_WIDTH - INITIAL_PACMAN_GAP - SPRITE_WIDTH, 'frame': 0, 'switch_rate': 5},
{'images': [ghostPink1, ghostPink2], 'x': -PACMAN_WIDTH - INITIAL_PACMAN_GAP - 2*(SPRITE_WIDTH + GAP), 'frame': 0, 'switch_rate': 7},
{'images': [ghostLightBlue1, ghostLightBlue2], 'x': -PACMAN_WIDTH - INITIAL_PACMAN_GAP - 3*(SPRITE_WIDTH + GAP), 'frame': 0, 'switch_rate': 9},
{'images': [ghostOrange1, ghostOrange2], 'x': -PACMAN_WIDTH - INITIAL_PACMAN_GAP - 4*(SPRITE_WIDTH + GAP), 'frame': 0, 'switch_rate': 11}
]
# Function to play the Pacman death animation
def play_pacman_dies(x_position):
for image in pacmanDies:
double_buffer.Clear()
double_buffer.SetImage(image, x_position)
matrix.SwapOnVSync(double_buffer)
time.sleep(PACMAN_DIES_DELAY)
# Main animation loop
while True:
double_buffer.Clear()
# Calculate the current gap between Pacman and the first ghost based on Pacman's position
pacman_position = sprites[0]['x']
if pacman_position < PACMAN_DIES_LOCATION:
# Linearly decrease the gap as Pacman approaches 2/3 of the way across the matrix
current_gap = INITIAL_PACMAN_GAP - (INITIAL_PACMAN_GAP - FINAL_PACMAN_GAP) * (pacman_position / PACMAN_DIES_LOCATION)
else:
# Once Pacman is beyond 2/3, the gap is at its minimum (overlap)
current_gap = FINAL_PACMAN_GAP
# Update positions of the ghosts based on the current gap
sprites[1]['x'] = sprites[0]['x'] - current_gap - SPRITE_WIDTH
sprites[2]['x'] = sprites[1]['x'] - (SPRITE_WIDTH + GAP)
sprites[3]['x'] = sprites[2]['x'] - (SPRITE_WIDTH + GAP)
sprites[4]['x'] = sprites[3]['x'] - (SPRITE_WIDTH + GAP)
# Check if the red ghost catches up to Pacman
if sprites[1]['x'] >= pacman_position - 2:
play_pacman_dies(pacman_position)
break
for sprite in sprites:
# Update the frame counter for each sprite
sprite['frame'] += 1
# Switch between the two images for ghosts or three images for Pacman based on the individual sprite's frame counter
image = sprite['images'][(sprite['frame'] // sprite['switch_rate']) % len(sprite['images'])]
# Draw the sprite at the current position
double_buffer.SetImage(image, sprite['x'])
# Move sprite to the right
sprite['x'] += 1
# Reset sprite position when it moves out of the right side of the matrix
if sprite['x'] > MATRIX_WIDTH:
# Reset positions, with Pacman leading and ghosts following with the initial gap
sprite['x'] = -PACMAN_WIDTH - INITIAL_PACMAN_GAP - (SPRITE_WIDTH + GAP) * (sprites.index(sprite) - 1)
# Swap the buffers to display the new frame
matrix.SwapOnVSync(double_buffer)
# Wait before the next frame
time.sleep(ANIMATION_DELAY)

