-
1BUTTON BOARD Assembly
![]()
![]()
![]()
- The button board assembly process begins by placing tactile switches on the Custom Button Board.
- Flipping the board over, we solder the tactile switch pads, permanently installing them in place.
-
2CONSOLE ASSEMBLY
![]()
![]()
![]()
![]()
- To begin the console assembly, detach the Left Holder section of the console and remove the screws that secure the holder to the PCB and RGB Matrix.
- We then installed the new 3D-printed Left Holder in place of the prior holder mounting position, using the same screws to secure it to the PCB and RGB matrix.
- By flipping the console over, we installed the button Board PCB with two M2 screws.
-
3WIRING
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
- For the button PCB wiring, we only need to connect both buttons to GPIO 27 and GPIO 28, with their common Ground connected to GND of PICO 2.
- For connections, we used long jumper wires connected to the button board's pads and then to the back side of the PICO DRIVER board.
- After the wires have been linked, we can enter the main code into our PICO and test the console.
-
4PICO BLASTER GAME CODE
This was the code we prepared for this project, and it's a simple but Long one.
#include <Adafruit_Protomatter.h> #include <SPI.h> #include <stdint.h> #include <math.h> // Matrix configuration #define WIDTH 64 #define HEIGHT 32 uint8_t rgbPins[] = {2, 3, 4, 5, 8, 9}; uint8_t addrPins[] = {10, 16, 18, 20}; #define CLK 11 #define LAT 12 #define OE 13 Adafruit_Protomatter matrix(WIDTH, HEIGHT, 1, rgbPins, 4, addrPins, CLK, LAT, OE, false); // Button pins #define BUTTON_UP 7 #define BUTTON_DOWN 6 #define BUTTON_LEFT 15 #define BUTTON_RIGHT 14 #define BUTTON_FIRE 27 #define BUTTON_MISSILE 28 // GPIO28 for missile // Game state variable bool gameOver = false; unsigned long gameOverStartTime = 0; const unsigned long gameOverDuration = 5000; // 5 seconds // Ship parameters #define SHIP_WIDTH 7 #define SHIP_HEIGHT 5 int shipX = 0; int shipY = HEIGHT / 2 - SHIP_HEIGHT / 2; // Projectile variables #define PROJECTILE_WIDTH 2 #define PROJECTILE_HEIGHT 2 #define MAX_PROJECTILES 5 int projX[MAX_PROJECTILES]; int projY[MAX_PROJECTILES]; bool projectileActive[MAX_PROJECTILES]; const uint16_t projectileColor = matrix.color565(0, 255, 0); int nextProjectile = 0; // Rock variables #define MAX_ROCKS 5 #define ROCK_SMALL_SIZE 3 #define ROCK_MEDIUM_SIZE 5 #define ROCK_LARGE_SIZE 7 int rocks[MAX_ROCKS][3]; // [x, y, size] unsigned long lastRockSpawn = 0; const unsigned long rockSpawnInterval = 500; const uint16_t rockColor = matrix.color565(255, 100, 0); const int rockSpeed = 1; int rockHitCount[MAX_ROCKS]; bool blastActive = false; // Add blast active flag int blastX, blastY; // Blast coordinates unsigned long blastStartTime; const unsigned long blastDuration = 100; //ms // Missile variables #define MISSILE_WIDTH 4 #define MISSILE_HEIGHT 4 int missileX = -1; int missileY = -1; bool missileActive = false; const uint16_t missileColor = matrix.color565(255, 0, 0); unsigned long lastMissileTime = 0; const unsigned long missileCooldown = 10000; // Spaceship sprite data (arrow pointing right) static const uint8_t shipSprite[SHIP_HEIGHT] = { 0b0010000, 0b0011000, 0b1111111, 0b0011000, 0b0010000 }; const uint16_t shipColor = matrix.color565(0, 255, 255); // Variables for fire rate control unsigned long lastFireTime = 0; const unsigned long fireRate = 200; // Function to draw a circle void drawCircle(int x0, int y0, int r, uint16_t color) { int f = 1 - r; int ddF_x = 1; int ddF_y = -2 * r; int x = 0; int y = r; matrix.drawPixel(x0, y0 + r, color); matrix.drawPixel(x0, y0 - r, color); matrix.drawPixel(x0 + r, y0, color); matrix.drawPixel(x0 - r, y0, color); while (x < y) { if (f >= 0) { y--; ddF_y += 2; f += ddF_y; } x++; ddF_x += 2; f += ddF_x; matrix.drawPixel(x0 + x, y0 + y, color); matrix.drawPixel(x0 - x, y0 + y, color); matrix.drawPixel(x0 + x, y0 - y, color); matrix.drawPixel(x0 - x, y0 - y, color); matrix.drawPixel(x0 + y, y0 + x, color); matrix.drawPixel(x0 - y, y0 + x, color); matrix.drawPixel(x0 + y, y0 - x, color); matrix.drawPixel(x0 - y, y0 - x, color); } } // Function to draw the blast animation void drawBlast() { if (blastActive) { matrix.drawPixel(blastX, blastY, matrix.color565(255, 255, 255)); matrix.drawPixel(blastX + 1, blastY, matrix.color565(255, 200, 0)); matrix.drawPixel(blastX - 1, blastY, matrix.color565(255, 200, 0)); matrix.drawPixel(blastX, blastY + 1, matrix.color565(255, 200, 0)); matrix.drawPixel(blastX, blastY - 1, matrix.color565(255, 200, 0)); if (millis() - blastStartTime > blastDuration) { blastActive = false; // Clear the blast after duration } } } // Function to draw text (using Adafruit_GFX style) void drawText(int x, int y, const char *text, uint16_t color) { matrix.setTextColor(color); matrix.setCursor(x, y); matrix.print(text); } // Function to draw the game over screen void drawGameOver() { matrix.fillScreen(0); // Clear the entire screen buffer // Draw a big circle for the face int centerX = WIDTH / 2; int centerY = HEIGHT / 2; int radius = 10; uint16_t circleColor = matrix.color565(255, 255, 0); // Yellow drawCircle(centerX, centerY, radius, circleColor); // Draw the sad eyes as crosses uint16_t eyeColor = matrix.color565(0, 0, 0); // Black matrix.drawPixel(centerX - 5, centerY - 5, eyeColor); matrix.drawPixel(centerX - 4, centerY - 4, eyeColor); matrix.drawPixel(centerX - 5, centerY - 4, eyeColor); matrix.drawPixel(centerX - 4, centerY - 5, eyeColor); matrix.drawPixel(centerX + 4, centerY - 5, eyeColor); matrix.drawPixel(centerX + 5, centerY - 4, eyeColor); matrix.drawPixel(centerX + 4, centerY - 4, eyeColor); matrix.drawPixel(centerX + 5, centerY - 5, eyeColor); // Draw the sad mouth for (int x = centerX - 4; x <= centerX + 4; x++) { matrix.drawPixel(x, centerY + 3, eyeColor); } matrix.show(); } // Setup function void setup() { matrix.begin(); matrix.fillScreen(0); Serial.begin(9600); pinMode(BUTTON_UP, INPUT_PULLUP); pinMode(BUTTON_DOWN, INPUT_PULLUP); pinMode(BUTTON_LEFT, INPUT_PULLUP); pinMode(BUTTON_RIGHT, INPUT_PULLUP); pinMode(BUTTON_FIRE, INPUT_PULLUP); pinMode(BUTTON_MISSILE, INPUT_PULLUP); // Initialize rocks for (int i = 0; i < MAX_ROCKS; i++) { rocks[i][0] = -ROCK_LARGE_SIZE * 2; // Initialize off-screen rocks[i][1] = 0; rocks[i][2] = ROCK_LARGE_SIZE; // Start with largest size rockHitCount[i] = 0; } // Initialize projectiles for (int i = 0; i < MAX_PROJECTILES; i++) { projX[i] = -PROJECTILE_WIDTH; projY[i] = -PROJECTILE_HEIGHT; projectileActive[i] = false; } missileX = -MISSILE_WIDTH; missileY = -MISSILE_HEIGHT; missileActive = false; gameOver = false; gameOverStartTime = 0; // Initialize Serial.println("Starting up..."); } // Function to draw the spaceship void drawShip() { if (!gameOver) { // Only draw if game is not over for (int y = 0; y < SHIP_HEIGHT; y++) { for (int x = 0; x < SHIP_WIDTH; x++) { if (bitRead(shipSprite[y], 6 - x)) { matrix.drawPixel(shipX + x, shipY + y, shipColor); } } } } } // Function to draw the projectiles void drawProjectiles() { for (int i = 0; i < MAX_PROJECTILES; i++) { if (projectileActive[i]) { matrix.fillRect(projX[i], projY[i], PROJECTILE_WIDTH, PROJECTILE_HEIGHT, projectileColor); } } } // Function to draw the missile void drawMissile() { if (missileActive) { matrix.fillRect(missileX, missileY, MISSILE_WIDTH, MISSILE_HEIGHT, missileColor); } } // Function to reset the game state void resetGame() { gameOver = false; gameOverStartTime = 0; shipX = 0; shipY = HEIGHT / 2 - SHIP_HEIGHT / 2; for (int i = 0; i < MAX_PROJECTILES; i++) { projX[i] = -PROJECTILE_WIDTH; projY[i] = -PROJECTILE_HEIGHT; projectileActive[i] = false; } for (int i = 0; i < MAX_ROCKS; i++) { rocks[i][0] = -ROCK_LARGE_SIZE * 2; // Reset all rocks offscreen rocks[i][1] = 0; rocks[i][2] = ROCK_LARGE_SIZE; rockHitCount[i] = 0; } missileX = -MISSILE_WIDTH; missileY = -MISSILE_HEIGHT; missileActive = false; lastRockSpawn = 0; lastFireTime = 0; lastMissileTime = 0; blastActive = false; } // Main loop void loop() { if (gameOver) { drawGameOver(); if (millis() - gameOverStartTime >= gameOverDuration) { resetGame(); } return; // Stop updating the game } matrix.fillScreen(0); // Movement handling if (!digitalRead(BUTTON_UP)) { shipY = max(shipY - 1, 0); } if (!digitalRead(BUTTON_DOWN)) { shipY = min(shipY + 1, HEIGHT - SHIP_HEIGHT); // Corrected variable name here } if (!digitalRead(BUTTON_LEFT)) { shipX = max(shipX - 1, 0); } if (!digitalRead(BUTTON_RIGHT)) { shipX = min(shipX + 1, WIDTH - SHIP_WIDTH); } // Firing projectiles if (!digitalRead(BUTTON_FIRE) && (millis() - lastFireTime >= fireRate)) { int projectileIndex = -1; for (int i = 0; i < MAX_PROJECTILES; i++) { if (!projectileActive[i]) { projectileIndex = i; break; } } if (projectileIndex != -1) { projX[projectileIndex] = shipX + SHIP_WIDTH; projY[projectileIndex] = shipY + SHIP_HEIGHT / 2 - PROJECTILE_HEIGHT / 2; projectileActive[projectileIndex] = true; lastFireTime = millis(); Serial.println("Fire!"); } } // Fire Missile if (!digitalRead(BUTTON_MISSILE) && (millis() - lastMissileTime >= missileCooldown) && !missileActive) { missileX = shipX + SHIP_WIDTH; missileY = shipY + SHIP_HEIGHT / 2 - MISSILE_HEIGHT / 2; missileActive = true; lastMissileTime = millis(); Serial.println("Missile Fire!"); } // Projectile movement for (int i = 0; i < MAX_PROJECTILES; i++) { if (projectileActive[i]) { projX[i] += 3; if (projX[i] >= WIDTH) { projectileActive[i] = false; projX[i] = -PROJECTILE_WIDTH; projY[i] = -PROJECTILE_HEIGHT; } } } // Missile movement if (missileActive) { missileX += 2; if (missileX >= WIDTH) { missileActive = false; missileX = -MISSILE_WIDTH; missileY = -MISSILE_HEIGHT; } } // Rock spawning if (millis() - lastRockSpawn > rockSpawnInterval) { int availableRockSlot = -1; for (int i = 0; i < MAX_ROCKS; i++) { if (rocks[i][0] <= -ROCK_LARGE_SIZE * 2) { // Check if rock is off-screen availableRockSlot = i; break; } } if (availableRockSlot != -1) { rocks[availableRockSlot][0] = WIDTH; rocks[availableRockSlot][1] = random(HEIGHT - ROCK_LARGE_SIZE * 2 + 1) + ROCK_LARGE_SIZE; rocks[availableRockSlot][2] = random(3) == 0 ? ROCK_SMALL_SIZE : (random(2) == 0 ? ROCK_MEDIUM_SIZE : ROCK_LARGE_SIZE); // Random size lastRockSpawn = millis(); } } // Rock handling and collision for (int i = 0; i < MAX_ROCKS; i++) { if (rocks[i][0] >= 0) { rocks[i][0] -= rockSpeed; int rockSize = rocks[i][2]; uint16_t drawColor = rockColor; if (rockSize == ROCK_SMALL_SIZE) { drawColor = matrix.color565(255, 140, 0); // Darker Orange #FF8C00 } else if (rockSize == ROCK_MEDIUM_SIZE) { drawColor = matrix.color565(255,255,0); // Yellow } else { drawColor = matrix.color565(255, 0, 0); // Red } drawCircle(rocks[i][0], rocks[i][1], rockSize, drawColor); // Game over check if (shipX < rocks[i][0] + rockSize && shipX + SHIP_WIDTH > rocks[i][0] - rockSize && shipY < rocks[i][1] + rockSize && shipY + SHIP_HEIGHT > rocks[i][1] - rockSize) { gameOver = true; gameOverStartTime = millis(); // Record start time Serial.println("Game Over - Ship hit by rock!"); break; // Exit the loop } // Check collision with projectiles for (int j = 0; j < MAX_PROJECTILES; j++) { if (projectileActive[j] && projX[j] < rocks[i][0] + rockSize && projX[j] + PROJECTILE_WIDTH > rocks[i][0] - rockSize && projY[j] < rocks[i][1] + rockSize && projY[j] + PROJECTILE_HEIGHT > rocks[i][1] - rockSize) { rockHitCount[i]++; projectileActive[j] = false; projX[j] = -PROJECTILE_WIDTH; projY[j] = -PROJECTILE_HEIGHT; Serial.println("Hit Rock!"); if ((rocks[i][2] == ROCK_SMALL_SIZE && rockHitCount[i] >= 1) || (rocks[i][2] == ROCK_MEDIUM_SIZE && rockHitCount[i] >= 2) || (rocks[i][2] == ROCK_LARGE_SIZE && rockHitCount[i] >= 3)) { // 3 hits for large rocks[i][0] = -ROCK_LARGE_SIZE * 2; rockHitCount[i] = 0; blastX = rocks[i][0]; // Store blast coordinates blastY = rocks[i][1]; blastActive = true; // Trigger blast blastStartTime = millis(); } break; } } // Check collision with missile if (missileActive && missileX < rocks[i][0] + rockSize && missileX + MISSILE_WIDTH > rocks[i][0] - rockSize && missileY < rocks[i][1] + rockSize && missileY + MISSILE_HEIGHT > rocks[i][1] - rockSize) { rocks[i][0] = -ROCK_LARGE_SIZE * 2; rockHitCount[i] = 0; missileActive = false; missileX = -MISSILE_WIDTH; missileY = -MISSILE_HEIGHT; Serial.println("Missile Hit Rock!"); blastX = rocks[i][0]; // Store blast coordinates blastY = rocks[i][1]; blastActive = true; // Trigger blast blastStartTime = millis(); } } } drawShip(); drawProjectiles(); drawMissile(); drawBlast(); // Draw blast matrix.show(); delay(50); }Setup for the LED Matrix Display
#define WIDTH 64#define HEIGHT 32uint8_t rgbPins[] = {2, 3, 4, 5, 8, 9};uint8_t addrPins[] = {10, 16, 18, 20};#define CLK 11#define LAT 12#define OE 13Adafruit_Protomatter matrix(WIDTH, HEIGHT, 1, rgbPins, 4, addrPins, CLK, LAT, OE, false);This Section Configures pins to control the 64x32 RGB LED matrix. the
Adafruit_Protomatterinitializes the matrix library for displaying pixels, text, or graphics and Control pins (CLK,LAT,OE) handle data timing and refresh rates.Spaceship Setup
#define SHIP_WIDTH 7#define SHIP_HEIGHT 5int shipX = 0;int shipY = HEIGHT / 2 - SHIP_HEIGHT / 2;
This Section Defines the spaceship's size and starting position (
shipX,shipY) on the screen. Initially, the ship is centered vertically.static const uint8_t shipSprite[SHIP_HEIGHT] = { 0b0010000, 0b0011000, 0b1111111, 0b0011000, 0b0010000};This is a binary representation of the spaceship sprite. Each row represents one horizontal layer of the ship (an arrow-like shape pointing right).
Projectile Handling
#define PROJECTILE_WIDTH 2#define PROJECTILE_HEIGHT 2#define MAX_PROJECTILES 5int projX[MAX_PROJECTILES];int projY[MAX_PROJECTILES];bool projectileActive[MAX_PROJECTILES];const uint16_t projectileColor = matrix.color565(0, 255, 0);int nextProjectile = 0;
This Sets up an array to track projectiles' positions and activation state (
projX,projY,projectileActive) and Defines a maximum of 5 active projectiles (MAX_PROJECTILES)Firing Logic
if (!digitalRead(BUTTON_FIRE) && (millis() - lastFireTime >= fireRate)) { int projectileIndex = -1; for (int i = 0; i < MAX_PROJECTILES; i++) { if (!projectileActive[i]) { projectileIndex = i; break; } } if (projectileIndex != -1) { projX[projectileIndex] = shipX + SHIP_WIDTH; projY[projectileIndex] = shipY + SHIP_HEIGHT / 2 - PROJECTILE_HEIGHT / 2; projectileActive[projectileIndex] = true; lastFireTime = millis(); Serial.println("Fire!"); }}This Section Checks button input (
BUTTON_FIRE) and ensures firing respects the cooldown (fireRate) and then Activates the next available projectile and positions it to fire from the center-right of the ship.Missile Initialization
#define MISSILE_WIDTH 4#define MISSILE_HEIGHT 4int missileX = -1;int missileY = -1;bool missileActive = false;const uint16_t missileColor = matrix.color565(255, 0, 0);unsigned long lastMissileTime = 0;const unsigned long missileCooldown = 10000;
This part Creates variables for missile position and activity. Initially, the missile is inactive (
missileActive = false).It also sets a longer cooldown compared to projectiles (
missileCooldown = 10 seconds).Missile Firing
if (!digitalRead(BUTTON_MISSILE) && (millis() - lastMissileTime >= missileCooldown) && !missileActive) { missileX = shipX + SHIP_WIDTH; missileY = shipY + SHIP_HEIGHT / 2 - MISSILE_HEIGHT / 2; missileActive = true; lastMissileTime = millis(); Serial.println("Missile Fire!");}This Fires a missile with a cooldown. The missile starts from the center-right edge of the ship.
Rock Initialization (Obstacle)
#define MAX_ROCKS 5#define ROCK_SMALL_SIZE 3#define ROCK_MEDIUM_SIZE 5#define ROCK_LARGE_SIZE 7int rocks[MAX_ROCKS][3]; // [x, y, size]unsigned long lastRockSpawn = 0;const unsigned long rockSpawnInterval = 500;const uint16_t rockColor = matrix.color565(255, 100, 0);const int rockSpeed = 1;
This part Sets up rock attributes: position (
rocks[i][0],rocks[i][1]), size (rocks[i][2]), and movement speed (rockSpeed = 1). Also, Rocks spawn every 500ms.Rock Movement
for (int i = 0; i < MAX_ROCKS; i++) { if (rocks[i][0] >= 0) { rocks[i][0] -= rockSpeed; }}This part Moves rocks leftward by subtracting their position (
rocks[i][0] -= rockSpeed). Rocks off-screen are reset for reuse.Collision Detection—Projectile vs Rock
if (projectileActive[j] && projX[j] < rocks[i][0] + rockSize && projX[j] + PROJECTILE_WIDTH > rocks[i][0] - rockSize && projY[j] < rocks[i][1] + rockSize && projY[j] + PROJECTILE_HEIGHT > rocks[i][1] - rockSize) { rockHitCount[i]++; projectileActive[j] = false; projX[j] = -PROJECTILE_WIDTH; projY[j] = -PROJECTILE_HEIGHT; Serial.println("Hit Rock!");}This Checks if a projectile intersects a rock. If true:
- Marks the projectile inactive.
- Tracks the number of hits the rock has taken.
Game Over Logic - When Ship Collides
if (shipX < rocks[i][0] + rockSize && shipX + SHIP_WIDTH > rocks[i][0] - rockSize && shipY < rocks[i][1] + rockSize && shipY + SHIP_HEIGHT > rocks[i][1] - rockSize) { gameOver = true; gameOverStartTime = millis(); Serial.println("Game Over - Ship hit by rock!");}This section ends the game when the ship collides with a rock and then displays the "Game Over" screen.
-
5RESULT
![]()
![]()
Here's the end result of this simple build: a working same invaders game that we built from scratch running on our CUSTOM GAME CONSOLE, which was also built from the ground up. The entire system is a DIY game console setup that can currently play two games: Snake and this new PICO BLASTER.
We can maneuver our spacecraft up, down, left, and right using the Direction Buttons, and we can fire bullets or blasts using the Weapon Buttons.
This game is a fun one and is totally playable. The screen refresh rate is also smooth and PICO 2 is handling this task quite well. If our spacecraft hits any projectile, we are greeted with a game over screen that shows a whole yellow circle showing that the circles WON. this splash screen stays for 5 seconds, and then the whole game resets.
This game is currently very basic and made up of simple geometrical shapes. We may further change this code by adding sprites to the game file, which are created using a pixel art generator and then turned into .H files. We can add more adversaries and objects to our game to improve its overall functionality.
Reach out to me if you require any extra assistance.
This game code, as well as the build instructions for this project or the snake game console, are accessible in this article, and it was previously published, so feel free to tweak the code and create your own game console.
Thanks for reaching this far, and I will be back with a new project pretty soon.
Peace.
Arnov Sharma

















Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.