Galactic Hero Game

Learn how to build your own Guitar Hero Clone with the Pimoroni Galactic Unicorn and MicroPython

Raspberry Pi Pico MicroPython Pimoroni Galactic Unicorn Game

13 December 2022 by Kevin McAleer | Share this article on


Contents


YouTube Video

Click the thumbnails below to watch the show all about this build.

Overview

Build your own Galactic Guitar Hero game with Pimoroni Galactic Unicorn (GU) and some MicroPython code. It’s fun and easy to enhance too.


Bill of Materials

For this project you’ll just need a Pimoroni Galactic Unicorn display. It features 153 pixel display, along with a Raspberry Pi Pico W aboard.

Item Description Qty Price
Galactic Unicorn Pimoroni Galactic Unicorn 1 £49.50

The Galactic Unicorn

Here are some of the main features of the Pimoroni Galactic Unicorn:

  • Raspberry Pi Pico W Aboard
  • 583 RGB LEDs in a 53 x 11 grid
  • 3.5mm LEDs with rounded square apertures
  • 6mm LED spacing
  • Driven by 10 FM6047 constant current LED drivers
  • MAX98357 3.2W I2S Mono Amplifier (with 30mm 1W speaker)
  • Phototransistor for light sensing
  • 9 tactile user buttons
  • Reset button
  • 2x Qw/ST (Qwiic/STEMMA QT) connectors
  • JST-PH connector for attaching a battery (5.5V max)
  • Fully assembled
  • No soldering required#

Front view of the Pimoroni Galactic Unicorn

Front view of the Pimoroni Galactic Unicorn


Build your own Galactic Hero game with Pimoroni Galactic Unicorn

Gameplay - How to win or loose

Galactic Hero gameplay is simliar to the original Guitar Hero:

  • Notes fall down the guitar neck towards the scoring line
  • The player presses the corresponding button for each column of notes as it passes the scoring line
  • The player looses a life with each note that is missed
  • Game stops when all lives are lost

Front view of the Pimoroni Galactic Unicorn

The scoring line is a white line on column 50 of the display, and each of the five columns (guitar frets) has a corresponding button at the bottom of the GU guitar neck.

Front view of the Pimoroni Galactic Unicorn


GPIO Pinouts

The GU MicroPython library has a number of constants to help with writing programs. Each of the nine user programmable buttons has a GPIO pin associated with it, and we will use just five of these in our game:

GPIO Pin Galactic Unicorn Function Galactic Guitar Hero Assignment
26 SWITCH_BRIGHTNESS_DOWN button_a
21 SWITCH_BRIGHTNESS_UP button_b
27 SWITCH_SLEEP button_c
8 SWITCH_VOLUME_DOWN button_d
7 SWITCH_VOLUME_UP button_e
0 SWITCH_A -
1 SWITCH_B -
3 SWITCH_C -
6 SWITCH_D -

Game Notes

The notes for the game are simply stored in a tuple. The notes are represented in Python in the way they will appear on screen, with a 1 representing a note for that fret position, and a 0 meaning no note.

To keep the code in the main program clean, the game notes are stored in a separate file: tune01.py, which must be copied to the GU; this can be easily done with Thonny (right click on the file and click upload to /).

tune = ('00001',
        '00010',
        '00100',
        '01000',
        '10000',
        '01000',
        '00100',
        '00010',
        '00001',
        '00010',
        '00100',
        '01000',
        '10000',
        '01000',
        '00100',
        '00010',
        '00001',
        )

PicoGraphics

We can use the excellent Pimoroni PicoGraphics MicroPython library to display our notes on the display. For this we will need to use three different functions.

Front view of the Pimoroni Galactic Unicorn

We can create colour pens using the function:

display.create_pen(red, green, blue)

create_pen is used to create a pen object that holds a set of colours. Pens are used by PicoGraphics to draw coloured pixels on displays.

We can set colours using the function:

display.set_pen(pen)

set_pen is used to set the pen with a colour and the next graphics operation (such as pixel) will use this colour.

We can draw pixels using the function:

display.pixel(x, y)

pixel actually lights up the pixel at the x and y position on the display, using the current pen colour.


MicroPython code

The main game loop

Below is the main game loop, we’ll look through each step to understand what is going on:

while lives >= 1 :
    display.set_pen(black)
    display.clear()
    display_board()
    display_tune(tune,x,y)
    check_missed()
    winning = check_buttons()
    if not winning:
        lives -= 1
        print('lost a life, lives remaining:',lives)
    x = x + 1
    gu.update(display)
    sleep(0.01)
    offset = x - len(tune[0])
    if offset > WIDTH-4:
        x = x_reset
    
print("you lost")
while lives >= 1 this means keep looping round until the lives variable is greater or equal to 1, when we have 0 lives the game will end.
display.set_pen(black) sets the current pen colour to black
display.clear() sets all the pixels on the display to the current pen colour, which is black
display_board() displays the game board, we’ll look at this below
display_tune(tune,x,y) displays the notes at the x and y position on the display, only the x position changes, as the notes fall down the display
check_missed() this is a function that checks to see if any notes have been missed and changes the score line to red.
winning = check_buttons() this is a function that checks to see if the player has pressed the corresponding buttons for any frets with notes in the scoring zone, if they have winning is True if they haven’t winning is False
if not winning: if the winning variable is not True then run the code block below
lives -= 1 take 1 life away from the play
print('lost a life, lives remaining:',lives) print the message to the console lost a life, lives remaining and the current value of lives
x = x + 1 increment the x position, so that the next time the notes are drawn, they appear to fall down a row
gu.update(display) update the display - this will make all the pixels appear in their new position
sleep(0.01) sleep for 100th of a second.
offset = x - len(tune[0]) update the offset to equal the current x value minus the lenth of the tune; we only need to check one row which is what tune[0] means - check the zeroth row length
if offset > WIDTH-4: check if the value of offset is greater than the value of (WIDTH minus 4)
x = x_reset reset the x position back to the value stored in x_reset - this makes the notes appear just off the top of the display ready to drop down on the new loop cycle
print("you lost") Print the message you lost to the console

display_board()

The display_board function displays the game board, which is just a white line at row 50 on the display.

def display_board():
    """ Display the gameboard """
    display.set_pen(white)
    display.line(50,0,50,11)
    display.update()

display_tune(tune,x,y)

The display_tune(tune,x,y) function takes three parameters

  • the tuple containin the notes
  • the x position to display the notes
  • the y position to displat the notes
def display_tune(bitmap, x:int, y:int):
    """ Display a tune on the screen at x,y coordinates """
    global fret_a, fret_b, fret_c, fret_d, fret_e
    row_offset = 0
    col_offset = 0
    fret_a = False
    fret_b = False
    fret_c = False
    fret_d = False
    fret_e = False
    # Loop through each row of the bitmap
    for row in bitmap:
        
        row_offset = 0
        fret_no = 0
        # Loop through each pixel in the row
        for pixel in row:
            
            # check if row is on screen, within the bounds of the display
            if len(row)-row_offset < WIDTH:
                if len(bitmap)+(col_offset//2) < HEIGHT:
                    if pixel == '1':
                        colour = fret_colours[col_offset//2]
                        
                        display.set_pen(colour)
                        
                        # set the game state
                        if x+row_offset == 50:
                            if y+col_offset == 0: fret_e = True
                            if y+col_offset == 2: fret_d = True
                            if y+col_offset == 4: fret_c = True
                            if y+col_offset == 6: fret_b = True
                            if y+col_offset == 8: fret_a = True
                    else:
                        
                        display.set_pen(black)

                    # display the pixel, double width
                    display.pixel(x+row_offset, y+col_offset)
                    display.pixel(x+row_offset, y+col_offset+1)
                        
            fret_no += 1
            row_offset += 2 
        col_offset += 2
global fret_a, fret_b, fret_c, fret_d, fret_e this allows the fret_a thorugh to fret_e global variables to be changed, normally any changes would only be made local to the function. The fret_x variables hold a True if a note is currently in the scoring zone, and False if not
row_offset = 0 set row_offset to zero
col_offset = 0 set col_offset to zero
fret_a = False set fret_a to False - this represents if a note on Fret a is in the scoring zone
fret_b = False set fret_b to False - this represents if a note on Fret b is in the scoring zone
fret_c = False set fret_c to False - this represents if a note on Fret c is in the scoring zone
fret_d = False set fret_d to False - this represents if a note on Fret d is in the scoring zone
fret_e = False set fret_e to False - this represents if a note on Fret e is in the scoring zone
for row in bitmap: for each row of the bitmap, run the code block below
row_offset = 0 reset the row_offset to zero
fret_no = 0 set the current fret_no (fret number) to zero
for pixel in row: for each pixel in the current row, run the code block below
if len(row)-row_offset < WIDTH: if the length of the current row minus the row_offset is less than the display WIDTH, run the code block below
if len(bitmap)+(col_offset//2) < HEIGHT: if the length the of the bitmap plus the col_offset divided by 2 is less than the display WIDTH, run the code block below
if pixel == '1': if the current pixel is the value 1, run the code block below
colour = fret_colours[col_offset//2] set the current colour to the correct colour for this fret, (col_offset divided by two is because we are stretching pixels twice the width above)
display.set_pen(colour) Set the pen colour to the fret colour.

check_buttons

def check_buttons():
    """ Check the buttons on the Galactic Unicorn """
    win = True
    button_a = GalacticUnicorn.SWITCH_BRIGHTNESS_DOWN
    button_b = GalacticUnicorn.SWITCH_BRIGHTNESS_UP
    button_c = GalacticUnicorn.SWITCH_SLEEP
    button_d = GalacticUnicorn.SWITCH_VOLUME_DOWN
    button_e = GalacticUnicorn.SWITCH_VOLUME_UP
        
    tests = {fret_a:button_a, fret_b:button_b, fret_c:button_c, fret_d:button_d, fret_e:button_e}

    for fret, button in tests.items():
        if fret:
            if gu.is_pressed(button):
                win = True
            else:
                win = False
                break
    return win
win = True Set the win to True; this is a flag to store the winning state
button_a = GalacticUnicorn.SWITCH_BRIGHTNESS_DOWN set button_a to the GU Brightness Down button
button_b = GalacticUnicorn.SWITCH_BRIGHTNESS_UP set button_b to the GU Brightness Up button
button_c = GalacticUnicorn.SWITCH_SLEEP set button_c to the GU Sleep button
button_d = GalacticUnicorn.SWITCH_VOLUME_DOWN set button_d to the GU Volumne Down button
button_e = GalacticUnicorn.SWITCH_VOLUME_UP set button_e to the GU Volume Up button
tests = {fret_a:button_a, fret_b:button_b, fret_c:button_c, fret_d:button_d, fret_e:button_e} create a list of fret states and matching buttons
for fret, button in tests.items(): for each fret and button, loop through items in tests list and run the code block below
if fret: if the current fret is True, run the code block below
if gu.is_pressed(button): if the current fret button is pressed, run the code block below
win = True set the win flag to True
else: otherwise
win = False set the win flag to False
break break out of the loop
return win return the current win flag status True or False

check_missed

def check_missed():
    """ Check if the note passed the bridge without a button being pressed """
    if fret_a or fret_b or fret_c or fret_d or fret_e:
        display.set_pen(red)
        display.rectangle(50,0,1,11)
        return True
    else:
        return False
if fret_a or fret_b or fret_c or fret_d or fret_e: if any of the frets are true; that is there is a note in the scoring of each fret
display.set_pen(red) set the pen colour to red
display.rectangle(50,0,1,11) display a red line on the scoring line
return True return True
else: otherwise
return False return False

Full Code listing

# Galactic Hero - A game for the Pimoroni Pico Display
# Kevin McAleer
# December 2022

from picographics import PicoGraphics, DISPLAY_GALACTIC_UNICORN as DISPLAY
from time import sleep
from galactic import GalacticUnicorn

# Set up the display
gu = GalacticUnicorn()
display = PicoGraphics(display=DISPLAY)
gu.set_brightness(0.25)
WIDTH, HEIGHT = display.get_bounds()

# Set up the colours
GREEN = {'red':0,'green':255,'blue':0}
RED = {'red':255,'green':0,'blue':0}
YELLOW = {'red':255,'green':255,'blue':0}
CYAN = {'red':0,'green':255,'blue':255}
ORANGE = {'red':255,'green':128,'blue':0}
BLACK = {'red':0,'green':0,'blue':0}
WHITE = {'red':255,'green':255, 'blue':255}

def create_pen(display, color):
    """ Create a pen from a colour dictionary """
    return display.create_pen(color['red'],color['green'],color['blue'])

# Create the pens
red = create_pen(display, RED)
green = create_pen(display, GREEN)
yellow = create_pen(display, YELLOW)
cyan = create_pen(display, CYAN)
orange = create_pen(display, ORANGE)
black = create_pen(display, BLACK)
white = create_pen(display, WHITE)

# Set the fret colours

fret_colours = {0:orange, 1:cyan, 2:yellow, 3:green, 4:red}

# Set the game button states
fret_a = False
fret_b = False
fret_c = False
fret_d = False
fret_e = False
winning = True

# Create the tune pattern - moved to a file
from tune01 import tune

def transpose(bitmap):
    """ Transpose a bitmap (This is because the Galactic Unicorn is rotated 90 degrees) """
    transposed_bitmap = [''.join([string[i] for string in bitmap]) for i in range(len(bitmap[0]))]
    return transposed_bitmap

def display_tune(bitmap, x:int, y:int):
    """ Display a tune on the screen at x,y coordinates """
    global fret_a, fret_b, fret_c, fret_d, fret_e
    row_offset = 0
    col_offset = 0
    fret_a = False
    fret_b = False
    fret_c = False
    fret_d = False
    fret_e = False
    # Loop through each row of the bitmap
    for row in bitmap:
        
        row_offset = 0
        fret_no = 0
        # Loop through each pixel in the row
        for pixel in row:
            
            # check if row is on screen, within the bounds of the display
            if len(row)-row_offset < WIDTH:
                if len(bitmap)+(col_offset//2) < HEIGHT:
                    if pixel == '1':
                        colour = fret_colours[col_offset//2]
                        
                        display.set_pen(colour)
                        
                        # set the game state
                        if x+row_offset == 50:
                            if y+col_offset == 0: fret_e = True
                            if y+col_offset == 2: fret_d = True
                            if y+col_offset == 4: fret_c = True
                            if y+col_offset == 6: fret_b = True
                            if y+col_offset == 8: fret_a = True
                    else:
                        
                        display.set_pen(black)

                    # display the pixel, double width
                    display.pixel(x+row_offset, y+col_offset)
                    display.pixel(x+row_offset, y+col_offset+1)
                        
            fret_no += 1
            row_offset += 2 
        col_offset += 2
        
def display_board():
    """ Display the gameboard """
    display.set_pen(white)
    display.line(50,0,50,11)
    display.update()
        
def fret_debug():
    if fret_a:
        display.set_pen(yellow)
        display.pixel(0,0)
    else:
        display.set_pen(black)
        display.pixel(0,0)
    if fret_b:
        display.set_pen(yellow)
        display.pixel(1,0)
    else:
        display.set_pen(black)
        display.pixel(1,0)
    if fret_c:
        display.set_pen(yellow)
        display.pixel(2,0)
    else:
        display.set_pen(black)
        display.pixel(2,0)
    if fret_d:
        display.set_pen(yellow)
        display.pixel(3,0)
    else:
        display.set_pen(black)
        display.pixel(3,0)
    if fret_e:
        display.set_pen(yellow)
        display.pixel(4,0)
    else:
        display.set_pen(black)
        display.pixel(4,0)
    display.update()
        
def check_buttons():
    """ Check the buttons on the Galactic Unicorn """
    win = True
    button_a = GalacticUnicorn.SWITCH_BRIGHTNESS_DOWN
    button_b = GalacticUnicorn.SWITCH_BRIGHTNESS_UP
    button_c = GalacticUnicorn.SWITCH_SLEEP
    button_d = GalacticUnicorn.SWITCH_VOLUME_DOWN
    button_e = GalacticUnicorn.SWITCH_VOLUME_UP
        
    tests = {fret_a:button_a, fret_b:button_b, fret_c:button_c, fret_d:button_d, fret_e:button_e}

    for fret, button in tests.items():
        if fret:
            if gu.is_pressed(button):
                win = True
            else:
                win = False
                break
    return win

def check_missed():
    """ Check if the note passed the bridge without a button being pressed """
    if fret_a or fret_b or fret_c or fret_d or fret_e:
        display.set_pen(red)
        display.rectangle(50,0,1,11)
        return True
    else:
        return False

# Transpose the tune
tune = transpose(tune)

# Set the starting position of the tune
x_reset = -len(tune[0]*2)
x = x_reset
y = 0

winning = True
lives = 3
print(f'Galactic Hero')

while lives >= 1 :
    display.set_pen(black)
    display.clear()
    display_board()
    display_tune(tune,x,y)
    check_missed()
    winning = check_buttons()
    if not winning:
        lives -= 1
        print('lost a life, lives remaining:',lives)
    x = x + 1
    gu.update(display)
    sleep(0.01)
    offset = x - len(tune[0])
    if offset > WIDTH-4:
        x = x_reset
    
print("you lost")

Did you find this content useful?


If you found this high quality content useful please consider supporting my work, so I can continue to create more content for you.

I give away all my content for free: Weekly video content on YouTube, 3d Printable designs, Programs and Code, Reviews and Project write-ups, but 98% of visitors don't give back, they simply read/watch, download and go. If everyone who reads or watches my content, who likes it, helps fund it just a little, my future would be more secure for years to come. A price of a cup of coffee is all I ask.

There are a couple of ways you can support my work financially:


If you can't afford to provide any financial support, you can also help me grow my influence by doing the following:


Thank you again for your support and helping me grow my hobby into a business I can sustain.
- Kevin McAleer