Arnov Sharma
Published © MIT

Pico VGA BLASTER

Made a Space Invaders-style game for my Custom VGA Driver Setup powered by PICO W

BeginnerFull instructions provided1 hour19
Pico VGA BLASTER

Things used in this project

Hardware components

Raspberry Pi Pico W
Raspberry Pi Pico W
×1
NextPCB  Custom PCB Board
NextPCB Custom PCB Board
×1

Software apps and online services

Arduino IDE
Arduino IDE
Fusion
Autodesk Fusion

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

STEP FILE

Schematics

Schematic

Code

code

C/C++
#include "vga_graphics.h"
#include "hardware/pwm.h"
#include "hardware/clocks.h"
#include "hardware/gpio.h"
#include "hardware/sync.h"
#include <stdio.h>
#include <string.h>

// ── Screen ────────────────────────────────────────────────────────────────────
#define SW 640
#define SH 480

// ── Buttons (active LOW — internal pull-up) ───────────────────────────────────
#define BTN_FIRE    0
#define BTN_MISSILE 2
#define BTN_LEFT    3
#define BTN_RIGHT   5
#define BTN_UP      1
#define BTN_DOWN    4

void buttonsInit() {
  const uint btns[] = {BTN_FIRE, BTN_MISSILE, BTN_LEFT, BTN_RIGHT, BTN_UP, BTN_DOWN};
  for (int i = 0; i < 6; i++) {
    gpio_init(btns[i]);
    gpio_set_dir(btns[i], GPIO_IN);
    gpio_pull_up(btns[i]);
  }
}
inline bool btnPressed(uint pin) { return !gpio_get(pin); }

// ── WS2812B LED Strip on GPIO 6 ───────────────────────────────────────────────
#define LED_PIN  6
#define NUM_LEDS 8  // <--- CHANGE THIS to the actual number of LEDs in your strip!

uint8_t strip_r[NUM_LEDS];
uint8_t strip_g[NUM_LEDS];
uint8_t strip_b[NUM_LEDS];

void ws2812Init() {
  gpio_init(LED_PIN);
  gpio_set_dir(LED_PIN, GPIO_OUT);
  gpio_put(LED_PIN, 0);
  for(int i=0; i<NUM_LEDS; i++) { strip_r[i]=0; strip_g[i]=0; strip_b[i]=0; }
}

// Custom bit-banger that perfectly respects VGA interrupts
static inline void ws2812_send_bit(bool bit) {
  if (bit) {
    uint32_t saved = save_and_disable_interrupts();
    gpio_put(LED_PIN, 1);
    asm volatile (
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
    ); // ~720ns High
    gpio_put(LED_PIN, 0);
    restore_interrupts(saved); // Allow VGA interrupt here!
    asm volatile (
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
    ); // Low
  } else {
    uint32_t saved = save_and_disable_interrupts();
    gpio_put(LED_PIN, 1);
    asm volatile (
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\n"
    ); // ~280ns High
    gpio_put(LED_PIN, 0);
    restore_interrupts(saved); // Allow VGA interrupt here!
    asm volatile (
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
      "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
    ); // Low
  }
}

// Set a specific LED color in memory
void setLedColor(int index, uint8_t r, uint8_t g, uint8_t b) {
  if(index >= 0 && index < NUM_LEDS) {
    strip_r[index] = r;
    strip_g[index] = g;
    strip_b[index] = b;
  }
}

// Push the memory to the actual strip
void showLeds() {
  for(int led = 0; led < NUM_LEDS; led++) {
    uint32_t grb = ((uint32_t)strip_g[led]<<16) | ((uint32_t)strip_r[led]<<8) | (uint32_t)strip_b[led];
    for (int i=23; i>=0; i--) ws2812_send_bit((grb>>i)&1);
  }
  gpio_put(LED_PIN, 0);
  sleep_us(60); // Latch signal
}

// LED fade state
bool          ledFadeActive = false;
unsigned long ledFadeStart = 0;
const unsigned long ledFadeDuration = 2000;  // total ms

void triggerLedFade() {
  ledFadeActive = true;
  ledFadeStart  = millis();
}

void updateLed() {
  static bool ledsAreOff = false;

  if (!ledFadeActive) { 
    if (!ledsAreOff) {
      for(int i=0; i<NUM_LEDS; i++) setLedColor(i, 0, 0, 0);
      showLeds();
      ledsAreOff = true;
    }
    return; 
  }
  
  ledsAreOff = false;
  unsigned long t = millis() - ledFadeStart;
  
  if (t >= ledFadeDuration) { 
    ledFadeActive = false; 
    for(int i=0; i<NUM_LEDS; i++) setLedColor(i, 0, 0, 0);
    showLeds();
    ledsAreOff = true;
    return; 
  }
  
  uint8_t brightness;
  if (t < ledFadeDuration / 2) {
    brightness = (uint8_t)((t * 255UL) / (ledFadeDuration / 2)); // fade in
  } else {
    brightness = (uint8_t)(((ledFadeDuration - t) * 255UL) / (ledFadeDuration / 2)); // fade out
  }
  
  // Set all LEDs to the current fade brightness
  for(int i=0; i<NUM_LEDS; i++) {
    setLedColor(i, brightness, 0, 0); 
  }
  showLeds(); 
}

// ── Buzzer ────────────────────────────────────────────────────────────────────
#define BUZZER_PIN 15

void buzzerInit() {
  gpio_set_function(BUZZER_PIN, GPIO_FUNC_PWM);
  uint slice = pwm_gpio_to_slice_num(BUZZER_PIN);
  pwm_set_enabled(slice, false);
}
void playTone(uint freq) {
  if (freq == 0) { pwm_set_enabled(pwm_gpio_to_slice_num(BUZZER_PIN), false); return; }
  uint slice   = pwm_gpio_to_slice_num(BUZZER_PIN);
  uint chan    = pwm_gpio_to_channel(BUZZER_PIN);
  uint32_t top = clock_get_hz(clk_sys) / freq - 1;
  if (top > 65535) top = 65535;
  pwm_set_wrap(slice, top);
  pwm_set_chan_level(slice, chan, top / 2);
  pwm_set_enabled(slice, true);
}
void stopTone() { pwm_set_enabled(pwm_gpio_to_slice_num(BUZZER_PIN), false); }

// ── Forward declarations ───────────────────────────────────────────────────────
void checkLives();
void drawGameOverScreen();
void startWave(int w);

// ── Non-blocking tone ─────────────────────────────────────────────────────────
unsigned long toneEndMs = 0;
void beep(uint freq, uint ms) { playTone(freq); toneEndMs = millis() + ms; }
void updateBuzzer() {
  if (toneEndMs && millis() >= toneEndMs) { stopTone(); toneEndMs = 0; }
}

// ── Intro music ───────────────────────────────────────────────────────────────
struct Note { uint freq; uint dur; };
const Note introMusic[] = {
  {330,120},{0,40},{262,120},{0,40},{220,120},{0,40},{196,200},{0,80},
  {330,100},{0,30},{294,100},{0,30},{262,150},{0,50},{247,120},{0,40},
  {220,300},{0,100},
  {392,100},{0,30},{370,100},{0,30},{330,150},{0,50},{294,120},{0,40},
  {262,80},{0,20},{247,80},{0,20},{220,80},{0,20},{196,300},{0,150},
  {165,120},{0,40},{196,120},{0,40},{220,120},{0,40},{247,120},{0,40},
  {262,200},{0,80},{220,400},{0,200}
};
const int NUM_NOTES      = sizeof(introMusic)/sizeof(Note);
int       musicNoteIdx   = 0;
unsigned long musicNoteEnd = 0;
bool      musicPlaying   = false;

void startIntroMusic() { musicNoteIdx=0; musicNoteEnd=0; musicPlaying=true; }
void stopIntroMusic()  { musicPlaying=false; stopTone(); toneEndMs=0; }
void updateIntroMusic() {
  if (!musicPlaying) return;
  if (millis() < musicNoteEnd) return;
  if (musicNoteIdx >= NUM_NOTES) musicNoteIdx = 0;
  const Note& n = introMusic[musicNoteIdx++];
  if (n.freq==0) stopTone(); else playTone(n.freq);
  musicNoteEnd = millis() + n.dur;
}

// ── SFX ───────────────────────────────────────────────────────────────────────
void sfxShoot()     { beep(1200,30); }
void sfxMissile()   { beep(500,80); }
void sfxAlienDie()  { beep(600,60);  triggerLedFade(); }
void sfxPlayerHit() { beep(200,200); }
void sfxBossHit()   { beep(400,80);  }
void sfxBossDie()   {
  triggerLedFade();
  for (int f=800;f>100;f-=80) { playTone(f); delay(30); }
  stopTone();
}
void sfxWave2Start() {
  for (int f=200;f<900;f+=60) { playTone(f); delay(25); }
  stopTone();
}

// ── Game States ───────────────────────────────────────────────────────────────
enum State { INTRO, LORE, PLAYING, GAMEOVER };
State gameState = INTRO;
unsigned long stateTimer = 0;

int score  = 0;
int lives  = 3;
int wave   = 1;
bool wave2Started = false;

// ── Starfield ─────────────────────────────────────────────────────────────────
#define NUM_STARS 60
struct Star { int x,y,speed; char color; };
Star stars[NUM_STARS];
int starWarp = 1;

// ── Debris ────────────────────────────────────────────────────────────────────
#define NUM_DEBRIS 5
struct Debris { int x,y,r,speed; char color; };
Debris debris[NUM_DEBRIS];
unsigned long lastDebrisMove = 0;
const int debrisMoveDelay    = 50;

// ── Player ────────────────────────────────────────────────────────────────────
int pX = 300, pOldX = 300;
int pY = 390, pOldY = 390;          
const int pW = 40, pH = 24, pSpeed = 5;
const int pYMin = 50;               
const int pYMax = SH - pH - 8;      

// ── Player Bullet ─────────────────────────────────────────────────────────────
int  bX=0, bY=0, bOldY=0;
bool bActive=false;
const int bSpeed=12, bW=4, bH=12;

// Debounce fire button
bool lastFireBtn   = false;
bool lastMissBtn   = false;

// ── Missile ───────────────────────────────────────────────────────────────────
#define MISSILE_MAX 5
int  missileAmmo   = MISSILE_MAX;
int  mX=0, mY=0, mOldY=0;
bool mActive=false;
const int mSpeed=6, mW=12, mH=20;

// ── Alien Army ────────────────────────────────────────────────────────────────
#define MAX_ROWS 5
#define MAX_COLS 10
bool alienAlive[MAX_ROWS][MAX_COLS];
int  alienRows=4, alienCols=8;
int  alienX=50, alienY=50, alienOldX=50, alienOldY=50;
const int aW=24, aH=16, aPad=14;
int  alienDir=1, alienSpeed=4;
unsigned long lastAlienMove=0;
int alienMoveDelay=40;

// ── Alien Bullets ─────────────────────────────────────────────────────────────
#define MAX_ABULLETS 5
struct AlienBullet { int x,y,oldY; bool active; };
AlienBullet abullets[MAX_ABULLETS];
unsigned long lastAlienFire=0;
int alienFireInterval=9999;

// ── Boss ──────────────────────────────────────────────────────────────────────
bool bossActive=false;
int  bossX=SW/2, bossY=130, bossOldX=SW/2, bossOldY=130;
int  bossR=24;
#define BOSS_HALF 56
int  bossHP=10, bossMaxHP=10;
int  bossHDir=1, bossVDir=1;
int  bossHSpeed=3, bossVSpeed=2;
const int bossYMin=60, bossYMax=230;
unsigned long lastBossMove=0;
int bossMoveDelay=25;
unsigned long lastVFlip=0;
int vFlipInterval=1200;
int  bbX=0, bbY=0, bbOldY=0;
bool bbActive=false;
int  bbSpeed=7;
const int bbW=6, bbH=14;
int  bossFireChance=35;
int  pinAnim=0;                    
unsigned long lastPinAnim=0;

// ── Lore ──────────────────────────────────────────────────────────────────────
char* loreLines[] = {
  "YEAR  2187.",
  "",
  "THEY CAME FROM THE SIGNAL.",
  "",
  "BEINGS BORN FROM CORRUPTED DATA,",
  "SHAPED LIKE THE VERY SCREENS",
  "THEY INVADED THROUGH.",
  "",
  "THE VGA ARMADA WIPED OUT",
  "EARTH'S DEFENCES IN 72 HOURS.",
  "",
  "ONE PILOT.  ONE SHIP.",
  "ONE LAST CHANCE.",
  "",
  "YOU ARE HUMANITY'S FINAL PIXEL."
};
const int NUM_LORE_LINES=15;
int loreCurLine=0, loreCurChar=0;
unsigned long lastCharTime=0;
bool loreAllDone=false;
unsigned long loreDoneTime=0;
uint loreBeepFreqs[]={900,950,880,960,840,1000,870,930,810,980};

// ── Helper: Draw Dual-Color Text ──────────────────────────────────────────────
void drawShadowText(int x, int y, const char* text, int size, char shadowCol, char textCol) {
  setTextSize(size);
  setTextColor(shadowCol);
  int offset = (size > 2) ? size / 2 : 1; 
  setTextCursor(x + offset, y + offset);
  writeString((char*)text);
  setTextColor(textCol);
  setTextCursor(x, y);
  writeString((char*)text);
}

// ── HUD ───────────────────────────────────────────────────────────────────────
void drawHeart(int cx, int cy, char color) {
  const char h[5][5]={{0,1,0,1,0},{1,1,1,1,1},{1,1,1,1,1},{0,1,1,1,0},{0,0,1,0,0}};
  for (int r=0;r<5;r++) for (int c=0;c<5;c++)
    if (h[r][c]) fillRect(cx+c*2, cy+r*2, 2, 2, color);
}

void drawMissileAmmoBar(bool force) {
  static int lastAmmo = -1;
  if (!force && missileAmmo == lastAmmo) return;
  lastAmmo = missileAmmo;
  int barW=14, barH=8, gap=3;
  int totalW = MISSILE_MAX*(barW+gap)-gap;
  int startX = SW-102;
  int startY = 32;   
  for (int i=0;i<MISSILE_MAX;i++) {
    int bx = startX + i*(barW+gap);
    char col = (i < missileAmmo) ? GREEN : BLACK;
    fillRect(bx, startY, barW, barH, col);
    drawRect(bx, startY, barW, barH, WHITE);
  }
}

void drawHUD(bool force) {
  static int  lastScore=-1, lastLives=-1, lastBossHP=-1;
  static bool lastBossActive=false;

  if (force||score!=lastScore) {
    char buf[20]; sprintf(buf,"SCORE %05d",score);
    fillRect(8,8,200,18,BLACK);
    setTextCursor(8,8); setTextColor(WHITE); setTextSize(2);
    writeString(buf);
    lastScore=score;
  }
  if (force||lives!=lastLives) {
    fillRect(SW-102,8,94,18,BLACK);
    for (int i=0;i<lives;i++) drawHeart((SW-94)+i*20,10,RED);
    lastLives=lives;
  }
  drawMissileAmmoBar(force);

  if (force) {
    char wbuf[10]; sprintf(wbuf,"W%d",wave);
    fillRect(SW/2-14,8,28,16,BLACK);
    setTextCursor(SW/2-14,8); setTextColor(YELLOW); setTextSize(2);
    writeString(wbuf);
  }
  if (bossActive) {
    if (force||bossHP!=lastBossHP||!lastBossActive) {
      fillRect(SW/2-65,28,130,10,BLACK);
      setTextSize(1); setTextColor(RED);
      setTextCursor(SW/2-48,28);
      writeString((char*)(wave==1?"VGA OVERLORD":"ARCH-OVERLORD"));
      fillRect(SW/2-52,39,104,12,BLACK);
      drawRect(SW/2-50,40,100,8,WHITE);
      if (bossHP>0) fillRect(SW/2-49,41,(bossHP*100)/bossMaxHP,6,RED);
      lastBossHP=bossHP; lastBossActive=true;
    }
  } else if (lastBossActive||force) {
    fillRect(SW/2-65,28,130,26,BLACK);
    lastBossHP=-1; lastBossActive=false;
  }
}

// ── Sprites ───────────────────────────────────────────────────────────────────
void drawShip(int x, int y, bool erase) {
  if (erase) { fillRect(x,y,pW,pH+8,BLACK); return; }
  fillRect(x+12,y+8,16,16,CYAN);
  fillRect(x+16,y,   8, 8,RED);
  fillRect(x,   y+12,8,12,WHITE);
  fillRect(x,   y+6, 8, 6,RED);
  fillRect(x+32,y+12,8,12,WHITE);
  fillRect(x+32,y+6, 8, 6,RED);
  if (millis()%200<100) fillRect(x+16,y+24, 8,8,YELLOW);
  else                  fillRect(x+14,y+24,12,6,RED);
}

void drawAlienSprite(int x, int y, bool erase, int row) {
  if (erase) { fillRect(x,y,aW,aH,BLACK); return; }
  const char w1c[4]={CYAN,MAGENTA,GREEN,YELLOW};
  const char w2c[5]={RED,YELLOW,MAGENTA,CYAN,WHITE};
  char c=(wave==1)?w1c[row%4]:w2c[row%5];
  const unsigned short spr[8]={
    0b01111111110,0b11000000011,0b10111111101,0b10101010101,
    0b10111111101,0b11000000011,0b01111111110,0b00011011000
  };
  for (int r=0;r<8;r++) {
    unsigned short line=spr[r];
    for (int col=0;col<11;col++)
      if (line&(1<<(10-col)))
        fillRect(x+1+col*2,y+r*2,2,2,c);
  }
}

void drawAliens(bool erase) {
  for (int r=0;r<alienRows;r++)
    for (int c=0;c<alienCols;c++)
      if (alienAlive[r][c]) {
        int x=(erase?alienOldX:alienX)+c*(aW+aPad);
        int y=(erase?alienOldY:alienY)+r*(aH+aPad);
        drawAlienSprite(x,y,erase,r);
      }
}

// ── VGA DB-15 Boss ────────────────────────────────────────────────────────────
void drawVGABoss(int cx, int cy, bool erase) {
  int bW = bossR*2 + 12;   
  int bH = bossR + 20;     
  int bx = cx - bW/2;
  int by = cy - bH/2;

  if (erase) {
    fillRect(cx-BOSS_HALF, cy-BOSS_HALF, BOSS_HALF*2, BOSS_HALF*2, BLACK);
    return;
  }

  char bodyCol = (wave==1) ? CYAN : MAGENTA;
  fillRect(bx, by, bW, bH, bodyCol);

  int notch=4;
  fillRect(bx,      by,      notch, notch, BLACK);  
  fillRect(bx+bW-notch, by, notch, notch, BLACK);  
  fillRect(bx,      by+bH-notch, notch, notch, BLACK);  
  fillRect(bx+bW-notch, by+bH-notch, notch, notch, BLACK); 

  char outlineCol = (wave==1) ? WHITE : YELLOW;
  drawRect(bx+notch, by,      bW-notch*2, bH,      outlineCol);
  drawRect(bx,       by+notch, bW,        bH-notch*2, outlineCol);

  int pinRows = 3, pinsPerRow = 5;
  int pinR    = 2;
  char pinCol = GREEN;
  for (int row=0; row<pinRows; row++) {
    int py = by + (bH*(row+1))/(pinRows+1);
    for (int p=0; p<pinsPerRow; p++) {
      int px = bx + (bW*(p+1))/(pinsPerRow+1);
      fillCircle(px, py, pinR, BLACK);
      drawCircle(px, py, pinR, pinCol);
    }
  }

  char pinNumStr[3];
  sprintf(pinNumStr, "%02d", pinAnim+1);
  char numCol = (wave==1) ? WHITE : RED;
  drawChar(cx-6, cy-4, pinNumStr[0], numCol, bodyCol, 1);
  drawChar(cx,   cy-4, pinNumStr[1], numCol, bodyCol, 1);

  if (wave==2) {
    drawRect(bx-4, by-4, bW+8, bH+8, RED);
    drawRect(bx-6, by-6, bW+12, bH+12, YELLOW);
  }
}

void drawExplosion(int x, int y) {
  drawCircle(x+aW/2,y+aH/2, 8,YELLOW);
  drawCircle(x+aW/2,y+aH/2,14,RED);
  delay(20);
  fillCircle(x+aW/2,y+aH/2,15,BLACK);
}
void drawBigExplosion(int x, int y) {
  fillCircle(x,y,20,YELLOW); delay(30);
  fillCircle(x,y,30,RED);    delay(30);
  fillCircle(x,y,32,BLACK);
}

// ── Missile draw ──────────────────────────────────────────────────────────────
void drawMissile(int x, int y, bool erase) {
  if (erase) { 
    fillRect(x-2, y-4, mW+4, mH+12, BLACK); 
    return; 
  }
  fillRect(x,   y,    mW,   mH,   YELLOW);
  fillRect(x+3, y-4,  mW-6, 4,    RED);     // nose
  fillRect(x-2, y+mH, 4,    4,    WHITE);   // left fin
  fillRect(x+mW-2, y+mH, 4, 4,   WHITE);   // right fin
  if (millis()%150<75) fillRect(x+2,y+mH+4,mW-4,4,YELLOW);
  else                  fillRect(x+2,y+mH+4,mW-4,4,RED);
}

// ── Background ────────────────────────────────────────────────────────────────
void updateStars() {
  for (int i=0;i<NUM_STARS;i++) {
    drawPixel(stars[i].x,stars[i].y,BLACK);
    stars[i].y+=stars[i].speed*starWarp;
    if (stars[i].y>=SH) { stars[i].y=50; stars[i].x=random(0,SW); }
    drawPixel(stars[i].x,stars[i].y,(starWarp>1)?WHITE:stars[i].color);
  }
}

void updateDebris() {
  if (millis()-lastDebrisMove<debrisMoveDelay) return;
  lastDebrisMove=millis();
  for (int i=0;i<NUM_DEBRIS;i++) {
    if (debris[i].y>48-debris[i].r) fillCircle(debris[i].x,debris[i].y,debris[i].r,BLACK);
    debris[i].y+=debris[i].speed;
    if (debris[i].y>SH+debris[i].r) {
      debris[i].y=random(-200,-40); debris[i].x=random(40,SW-40);
      debris[i].r=random(2,5);      debris[i].speed=random(1,3);
    }
    if (debris[i].y>48-debris[i].r) fillCircle(debris[i].x,debris[i].y,debris[i].r,debris[i].color);
  }
}

// ── Alien bullets ─────────────────────────────────────────────────────────────
void clearAlienBullets() { for (int i=0;i<MAX_ABULLETS;i++) abullets[i].active=false; }

void updateAlienBullets() {
  if (wave<2) return;
  if (millis()-lastAlienFire>(unsigned long)alienFireInterval) {
    lastAlienFire=millis();
    int col=random(0,alienCols), firingRow=-1;
    for (int r=alienRows-1;r>=0;r--) if (alienAlive[r][col]) { firingRow=r; break; }
    if (firingRow>=0) {
      for (int i=0;i<MAX_ABULLETS;i++) {
        if (!abullets[i].active) {
          abullets[i].active=true;
          abullets[i].x=alienX+col*(aW+aPad)+aW/2-2;
          abullets[i].y=alienY+firingRow*(aH+aPad)+aH;
          abullets[i].oldY=abullets[i].y;
          break;
        }
      }
    }
  }
  for (int i=0;i<MAX_ABULLETS;i++) {
    if (!abullets[i].active) continue;
    fillRect(abullets[i].x,abullets[i].oldY,4,10,BLACK);
    abullets[i].y+=5; abullets[i].oldY=abullets[i].y;
    if (abullets[i].y>SH) { abullets[i].active=false; }
    else if (abullets[i].x+4>pX&&abullets[i].x<pX+pW&&
             abullets[i].y+10>pY&&abullets[i].y<pY+pH) {
      fillRect(abullets[i].x,abullets[i].y,4,10,BLACK);
      abullets[i].active=false;
      lives--; sfxPlayerHit(); drawHUD(true);
      drawBigExplosion(pX+pW/2,pY+pH/2); checkLives();
    } else {
      fillRect(abullets[i].x,abullets[i].y,4,10,MAGENTA);
    }
  }
}

// ── Game Logic ────────────────────────────────────────────────────────────────
void checkLives() {
  if (lives<=0) {
    stopIntroMusic();
    drawGameOverScreen();
    gameState=GAMEOVER; stateTimer=millis();
  }
}

void updateAliens() {
  if (bossActive) return;
  if (millis()-lastAlienMove<(unsigned long)alienMoveDelay) return;
  lastAlienMove=millis();
  alienOldX=alienX; alienOldY=alienY;
  alienX+=alienDir*alienSpeed;
  bool hitEdge=false;
  for (int r=0;r<alienRows;r++)
    for (int c=0;c<alienCols;c++)
      if (alienAlive[r][c]) {
        int ax=alienX+c*(aW+aPad);
        if (ax<=10||ax+aW>=SW-10) hitEdge=true;
      }
  if (hitEdge) { alienDir*=-1; alienX+=alienDir*alienSpeed; alienY+=14; }
  drawAliens(true); drawAliens(false);
}

void updateBoss() {
  if (!bossActive) return;
  if (millis()-lastPinAnim>150) {
    lastPinAnim=millis();
    pinAnim=(pinAnim+1)%15;
  }
  if (millis()-lastBossMove>(unsigned long)bossMoveDelay) {
    lastBossMove=millis();
    if (millis()-lastVFlip>(unsigned long)vFlipInterval) {
      lastVFlip=millis(); bossVDir=(random(0,2)==0)?1:-1;
      vFlipInterval=700+random(0,700);
    }
    bossOldX=bossX; bossOldY=bossY;
    bossX+=bossHDir*bossHSpeed; bossY+=bossVDir*bossVSpeed;
    if (bossX-bossR<20||bossX+bossR>SW-20) { bossHDir*=-1; bossX+=bossHDir*bossHSpeed*2; }
    if (bossY<bossYMin) { bossY=bossYMin; bossVDir=1; }
    if (bossY>bossYMax) { bossY=bossYMax; bossVDir=-1; }
    drawVGABoss(bossOldX,bossOldY,true);
    drawVGABoss(bossX,bossY,false);
  }
  if (!bbActive&&random(0,bossFireChance)==1) {
    bbActive=true; bbX=bossX-bbW/2;
    bbY=bossY+(bossR+20)/2+12; bbOldY=bbY;
  }
  if (bbActive) {
    fillRect(bbX,bbOldY,bbW,bbH,BLACK);
    bbY+=bbSpeed; bbOldY=bbY;
    if (bbY>SH) { bbActive=false; }
    else if (bbX+bbW>pX&&bbX<pX+pW&&bbY+bbH>pY&&bbY<pY+pH) {
      fillRect(bbX,bbY,bbW,bbH,BLACK); bbActive=false;
      lives--; sfxPlayerHit(); drawHUD(true);
      drawBigExplosion(pX+pW/2,pY+pH/2); checkLives();
    } else {
      fillRect(bbX,bbY,bbW,bbH,wave==1?RED:YELLOW);
    }
  }
}

// ── Player Collisions ─────────────────────────────────────────────────────────
void checkPlayerCollisions() {
  bool crashed = false;

  if (bossActive) {
    int bW2 = bossR*2 + 12;
    int bH2 = bossR + 20;
    int bx = bossX - bW2/2;
    int by = bossY - bH2/2;
    if (pX < bx + bW2 && pX + pW > bx && pY < by + bH2 && pY + pH > by) {
      crashed = true;
    }
  } else {
    for (int r = 0; r < alienRows; r++) {
      for (int c = 0; c < alienCols; c++) {
        if (alienAlive[r][c]) {
          int ax = alienX + c*(aW+aPad);
          int ay = alienY + r*(aH+aPad);
          if (pX < ax + aW && pX + pW > ax && pY < ay + aH && pY + pH > ay) {
            crashed = true;
            alienAlive[r][c] = false; 
            fillRect(ax, ay, aW, aH, BLACK);
          }
        }
      }
    }
  }

  if (crashed) {
    lives = 0; // Instant game over
    sfxPlayerHit();
    drawBigExplosion(pX + pW/2, pY + pH/2);
    drawHUD(true);
    checkLives(); 
  }
}

// ── Bullet (normal) ───────────────────────────────────────────────────────────
void updateBullet() {
  if (!bActive) return;
  fillRect(bX,bOldY,bW,bH,BLACK);
  if (bY<50) { bActive=false; return; }
  bY-=bSpeed;
  bool hit=false;
  if (bossActive) {
    int bW2=bossR*2+12, bH2=bossR+20;
    if (bX+bW>bossX-bW2/2&&bX<bossX+bW2/2&&bY+bH>bossY-bH2/2&&bY<bossY+bH2/2) {
      bActive=false; hit=true; bossHP--;
      sfxBossHit(); drawHUD(true);
      drawExplosion(bossX-10,bossY+10);
      if (bossHP<=0) {
        bossActive=false; score+=500;
        drawVGABoss(bossX,bossY,true);
        sfxBossDie(); drawBigExplosion(bossX,bossY); drawHUD(true);
      }
    }
  } else {
    for (int r=0;r<alienRows&&!hit;r++)
      for (int c=0;c<alienCols&&!hit;c++)
        if (alienAlive[r][c]) {
          int ax=alienX+c*(aW+aPad), ay=alienY+r*(aH+aPad);
          bool hitNow =(bX<ax+aW&&bX+bW>ax&&bY<ay+aH&&bY+bH>ay);
          bool hitPrev=(bX<ax+aW&&bX+bW>ax&&bY+bSpeed<ay+aH&&bY+bSpeed+bH>ay);
          if (hitNow||hitPrev) {
            alienAlive[r][c]=false; hit=true; bActive=false;
            fillRect(ax,ay,aW,aH,BLACK);
            drawExplosion(ax,ay); sfxAlienDie(); score+=15;
          }
        }
  }
  if (!hit) { fillRect(bX,bY,bW,bH,YELLOW); bOldY=bY; }
}

// ── Missile update ────────────────────────────────────────────────────────────
void updateMissile() {
  if (!mActive) return;
  drawMissile(mX,mOldY,true);
  if (mY<50) { mActive=false; return; }
  mY-=mSpeed; mOldY=mY;
  bool hit=false;

  if (bossActive) {
    int bW2=bossR*2+12, bH2=bossR+20;
    if (mX+mW>bossX-bW2/2&&mX<bossX+bW2/2&&mY+mH>bossY-bH2/2&&mY<bossY+bH2/2) {
      mActive=false; hit=true;
      bossHP-=3; if (bossHP<0) bossHP=0;
      sfxBossHit(); drawHUD(true);
      drawExplosion(bossX-10,bossY+10);
      if (bossHP<=0) {
        bossActive=false; score+=500;
        drawVGABoss(bossX,bossY,true);
        sfxBossDie(); drawBigExplosion(bossX,bossY); drawHUD(true);
      }
    }
  } else {
    for (int r=0;r<alienRows;r++)
      for (int c=0;c<alienCols;c++)
        if (alienAlive[r][c]) {
          int ax=alienX+c*(aW+aPad), ay=alienY+r*(aH+aPad);
          if (mX<ax+aW&&mX+mW>ax&&mY<ay+aH&&mY+mH>ay) {
            alienAlive[r][c]=false;
            fillRect(ax,ay,aW,aH,BLACK);
            drawExplosion(ax,ay); sfxAlienDie(); score+=15;
            hit=true; 
          }
        }
    hit=false;
  }
  if (!hit) drawMissile(mX,mY,false);
  else      drawMissile(mX,mY,false); 
}

// ── Player (button-controlled) ────────────────────────────────────────────────
void updatePlayer() {
  pOldX=pX; pOldY=pY;

  if (btnPressed(BTN_LEFT))  pX-=pSpeed;
  if (btnPressed(BTN_RIGHT)) pX+=pSpeed;
  if (btnPressed(BTN_UP))    pY-=pSpeed;
  if (btnPressed(BTN_DOWN))  pY+=pSpeed;

  if (pX<10)       pX=10;
  if (pX+pW>SW-10) pX=SW-pW-10;
  if (pY<pYMin)    pY=pYMin;
  if (pY>pYMax)    pY=pYMax;

  if (pOldX!=pX || pOldY!=pY) drawShip(pOldX,pOldY,true);
  drawShip(pX,pY,false);

  bool fireNow = btnPressed(BTN_FIRE);
  if (fireNow && !lastFireBtn && !bActive) {
    bActive=true;
    bX=pX+pW/2-bW/2; bY=pY-bH; bOldY=bY;
    sfxShoot();
  }
  lastFireBtn=fireNow;

  bool missNow = btnPressed(BTN_MISSILE);
  if (missNow && !lastMissBtn && !mActive && missileAmmo>0) {
    mActive=true; missileAmmo--;
    mX=pX+pW/2-mW/2; mY=pY-mH-4; mOldY=mY;
    sfxMissile(); drawHUD(true);
  }
  lastMissBtn=missNow;
}

// ── Wave / game flow ──────────────────────────────────────────────────────────
void startWave(int w) {
  wave=w;
  alienRows=(w==1)?4:5; alienCols=(w==1)?8:10;
  alienX=50; alienOldX=50; alienY=50; alienOldY=50; alienDir=1;
  alienSpeed=(w==1)?4:5; alienMoveDelay=(w==1)?40:28;
  alienFireInterval=(w==1)?9999:800;
  for (int r=0;r<MAX_ROWS;r++)
    for (int c=0;c<MAX_COLS;c++)
      alienAlive[r][c]=(r<alienRows&&c<alienCols);
  clearAlienBullets();
  bossHP=(w==1)?10:20; bossMaxHP=bossHP;
  bossR=(w==1)?24:32;
  bossHSpeed=(w==1)?3:5; bossVSpeed=(w==1)?2:3;
  bossMoveDelay=(w==1)?25:18; bossFireChance=(w==1)?35:20;
  bbSpeed=(w==1)?7:10; bossActive=false;
  bossX=SW/2; bossOldX=bossX; bossY=bossYMin+20; bossOldY=bossY;
  bossHDir=1; bossVDir=1; bbActive=false;
  mActive=false; lastVFlip=millis(); vFlipInterval=1200;
  missileAmmo=MISSILE_MAX;
}

void checkGameState() {
  if (!bossActive&&bossHP<=0) {
    if (wave==1&&!wave2Started) {
      wave2Started=true;
      delay(500); clearScreen();
      setTextSize(4); setTextColor(RED);
      setTextCursor(100,180); writeString((char*)"WAVE  2");
      setTextSize(2); setTextColor(YELLOW);
      setTextCursor(80,250); writeString((char*)"THE ARCH-OVERLORD AWAKENS");
      sfxWave2Start(); delay(2500);
      clearScreen(); startWave(2); drawHUD(true); return;
    }
  }
  if (!bossActive&&bossHP>0) {
    bool anyAlive=false;
    for (int r=0;r<alienRows;r++)
      for (int c=0;c<alienCols;c++)
        if (alienAlive[r][c]) anyAlive=true;
    if (!anyAlive) {
      bossActive=true;
      bossX=SW/2; bossOldX=bossX; bossY=bossYMin+20; bossOldY=bossY;
      bossHDir=1; bossVDir=1; lastVFlip=millis(); drawHUD(true);
    }
  }
}

// ── Screens ───────────────────────────────────────────────────────────────────
void drawIntroScreen() {
  clearScreen();
  for (int i=0;i<NUM_STARS;i++) drawPixel(stars[i].x,stars[i].y,stars[i].color);
  setTextSize(8); setTextColor(YELLOW);
  setTextCursor(224,30);  writeString((char*)"PICO");
  setTextCursor(248,114); writeString((char*)"VGA");
  setTextCursor(152,198); writeString((char*)"BLASTER");
  setTextSize(2); setTextColor(WHITE);
  setTextCursor(190,360); writeString((char*)"DEFEND  HUMANITY");
  setTextSize(2); setTextColor(GREEN);
  setTextCursor(148,420); writeString((char*)"INCOMING  TRANSMISSION...");
}

void drawLoreBackground() {
  clearScreen();
  for (int i=0;i<NUM_STARS;i++) drawPixel(stars[i].x,stars[i].y,stars[i].color);
  setTextSize(2); setTextColor(YELLOW);
  setTextCursor(SW/2-96,14); writeString((char*)"// INTEL  UPLINK //");
  setTextSize(1); setTextColor(CYAN);
  setTextCursor(20,SH-16); writeString((char*)"END TRANSMISSION -- LAUNCHING MISSION");
}

bool tickLoreTypewriter() {
  if (loreCurLine>=NUM_LORE_LINES) return true;
  if (millis()-lastCharTime<38) return false;
  lastCharTime=millis();
  char* line=loreLines[loreCurLine];
  int lineLen=strlen(line);
  if (lineLen==0) { loreCurLine++; loreCurChar=0; return false; }
  if (loreCurChar<lineLen) {
    int ty=42+loreCurLine*24;
    setTextSize(2); setTextColor(GREEN);
    setTextCursor(30+loreCurChar*12,ty);
    char buf[2]={line[loreCurChar],'\0'};
    writeString(buf);
    beep(loreBeepFreqs[loreCurChar%10],18);
    loreCurChar++;
  } else { loreCurLine++; loreCurChar=0; }
  return false;
}

// ── Epic Game Over Screen ────────────────────────────────────────────────
void drawGameOverScreen() {
  clearScreen();
  for (int i=0;i<NUM_STARS;i++) drawPixel(stars[i].x,stars[i].y,stars[i].color);
  
  drawShadowText(104, 80, "GAME OVER", 8, RED, WHITE);
  drawShadowText(224, 180, "YOU LOST", 4, RED, MAGENTA);
  
  drawShadowText(200, 250, "HUMANITY HAS FALLEN.", 2, BLUE, CYAN);
  drawShadowText(200, 290, "THE VGA ARMADA WINS.", 2, BLUE, WHITE);
  
  char buf[30]; sprintf(buf,"FINAL SCORE: %05d",score);
  int bx = SW/2 - (int)(strlen(buf)*9); 
  drawShadowText(bx, 350, buf, 3, RED, YELLOW);
  
  drawShadowText(206, 410, "RESTARTING IN  5s...", 2, BLUE, GREEN);
  
  for (int f=500;f>80;f-=40) { playTone(f); delay(40); }
  stopTone();
}

void resetAllGameState() {
  score=0; lives=3; wave2Started=false;
  pX=300; pOldX=300; pY=390; pOldY=390; bActive=false; mActive=false;
  missileAmmo=MISSILE_MAX;
  startWave(1); lastAlienFire=0;
}

// ── Setup ─────────────────────────────────────────────────────────────────────
void setup() {
  initVGA();
  buzzerInit();
  buttonsInit();
  ws2812Init();
  clearScreen();
  for (int i=0;i<NUM_STARS;i++) {
    stars[i].x=random(0,SW); stars[i].y=random(50,SH);
    stars[i].speed=random(1,3);
    stars[i].color=(random(0,3)==0)?CYAN:WHITE;
  }
  clearAlienBullets();
  startWave(1);
  starWarp=1;
  drawIntroScreen();
  startIntroMusic();
  gameState=INTRO; stateTimer=millis();
}

// ── Main Loop ─────────────────────────────────────────────────────────────────
void loop() {
  updateBuzzer();
  updateLed();

  if (gameState==INTRO) {
    updateIntroMusic();
    updateStars();
    if (millis()-stateTimer>4000) {
      stopIntroMusic();
      loreCurLine=0; loreCurChar=0; loreAllDone=false;
      lastCharTime=millis();
      drawLoreBackground();
      gameState=LORE; stateTimer=millis();
    }
  }
  else if (gameState==LORE) {
    updateStars();
    bool done=tickLoreTypewriter();
    if (done&&!loreAllDone) {
      loreAllDone=true; loreDoneTime=millis();
      setTextSize(2); setTextColor(WHITE);
      setTextCursor(SW/2-110,SH-50);
      writeString((char*)"GOOD LUCK, PILOT...");
      beep(440,400);
    }
    if (loreAllDone&&millis()-loreDoneTime>2500) {
      clearScreen();
      for (int i=0;i<NUM_STARS;i++)
        drawPixel(stars[i].x,stars[i].y,stars[i].color);
      starWarp=1; drawHUD(true);
      gameState=PLAYING;
    }
  }
  else if (gameState==PLAYING) {
    checkGameState();
    updatePlayer();
    updateBullet();
    updateMissile();
    updateAliens();
    updateAlienBullets();
    updateBoss();
    checkPlayerCollisions();
    drawHUD(false);
  }
  else if (gameState==GAMEOVER) {
    if (millis()-stateTimer>5000) {
      resetAllGameState();
      starWarp=1;
      for (int i=0;i<NUM_STARS;i++) {
        stars[i].x=random(0,SW); stars[i].y=random(50,SH);
        stars[i].speed=random(1,3);
        stars[i].color=(random(0,3)==0)?CYAN:WHITE;
      }
      drawIntroScreen();
      startIntroMusic();
...

This file has been truncated, please download it to see its full contents.

Credits

Arnov Sharma
382 projects • 412 followers
I'm Arnov. I build, design, and experiment with tech—3D printing, PCB design, and retro consoles are my jam.

Comments