Brian Wente
Published © GPL3+

Retro Arcade Clock on a CYD

A retro arcade style clock built on a CYD with pixel enemies, flying saucers, missiles, explosions, and a score display that tells the time.

IntermediateFull instructions provided1 hour11
Retro Arcade Clock on a CYD

Things used in this project

Hardware components

Elegoo 2.8 inch TFT LCD Touch Display ESP32 board
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

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

Story

Read more

Code

Invader Clock

Arduino
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <WiFi.h>
#include <time.h>

//----------------------------------------
// TFT Screen Pins (E32R28T / E32N28T 2.8" ESP32-32E ILI9341 board)
//----------------------------------------
#define TFT_CS    15
#define TFT_DC    2
#define TFT_RST   -1
#define TFT_BL    21
#define TFT_SCLK  14
#define TFT_MOSI  13
#define TFT_MISO  12

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);

//----------------------------------------
// Wi-Fi Credentials
//----------------------------------------
const char* ssid     = "SSID";
const char* password = "PASSWORD";

const char* tzInfo     = "EST5EDT,M3.2.0,M11.1.0";
const char* ntpServer1 = "pool.ntp.org";
const char* ntpServer2 = "time.nist.gov";

//----------------------------------------
// Layout and Gameplay Constants
//----------------------------------------
#define SCREEN_WIDTH         240
#define SCREEN_HEIGHT        320
#define ROWS                 6
#define COLS                 10

#define INVADER_WIDTH        10
#define INVADER_HEIGHT       10
#define INVADER_X_SPACING    10
#define INVADER_Y_SPACING    10
#define INVADER_LEFT_MARGIN  20
#define INVADER_TOP_MARGIN   110

#define BASE_WIDTH           20
#define BASE_HEIGHT          10

#define MISSILE_WIDTH        2
#define MISSILE_HEIGHT       10

#define UFO_WIDTH            16
#define UFO_HEIGHT           7

const unsigned long frameSwitchInterval     = 500;
const unsigned long invaderShotInterval     = 8000;
const unsigned long nonDestructiveInterval  = 15000;
const unsigned long wifiConnectTimeout      = 15000;
const unsigned long ntpSyncTimeout          = 10000;
const unsigned long syncRetryInterval       = 30000;
const unsigned long flashFrameInterval      = 60;
const unsigned long explosionFrameInterval  = 50;
const unsigned long endOfWaveResetDelay     = 1500;
const int invaderMissMinX                   = 6;
const int invaderMissMaxX                   = SCREEN_WIDTH - 7;
const int invaderMissSafetyMargin           = 12;
const int invaderMissRedirectZone           = 32;
const int cannonDodgeSpeed                  = 6;

int  ufoX      = -1;
int  ufoY      = 64;
bool ufoActive = false;

bool          invaderFrame    = false;
unsigned long lastFrameSwitch = 0;

bool invaders[ROWS][COLS];
int  lastMinute = -1;
int  displayedScoreHour = 0;
int  displayedScoreMinute = 0;
bool scoreInitialized = false;
int  pendingScoreUpdates = 0;

struct tm currentTimeInfo = {};
bool      clockHasValidTime = false;
unsigned long lastTimeRecoveryAttempt = 0;
bool pendingDestructiveShot = false;
bool endOfWavePending = false;
unsigned long endOfWaveResetAt = 0;

// Cannon
int  cannonX         = SCREEN_WIDTH / 2 - BASE_WIDTH / 2;
int  cannonDirection = 1;
int  targetX         = -1;
bool topMinuteFire   = false;
bool cannonAligning  = false;
bool cannonDodging   = false;
int  dodgeTargetX    = -1;

// Player missile
int missileX = -1;
int missileY = -1;
bool missileImpactPending = false;

// Invader missile
int invMissileX       = -1;
int invMissileY       = -1;
int invMissileTargetX = -1;
unsigned long lastInvaderShot = 0;
unsigned long lastNonDestructiveAttempt = 0;

// Non-blocking flash/explosion effects
bool flashActive = false;
uint8_t flashFrame = 0;
unsigned long lastFlashTick = 0;

bool explosionActive = false;
int explosionX = -1;
int explosionY = -1;
uint8_t explosionFrame = 0;
unsigned long lastExplosionTick = 0;

uint16_t invaderColors[ROWS] = {
  ILI9341_RED, 0xFC60, ILI9341_YELLOW,
  ILI9341_GREEN, ILI9341_CYAN, ILI9341_PURPLE
};

//----------------------------------------
// Forward Declarations
//----------------------------------------
bool connectWiFiWithTimeout(unsigned long timeoutMs);
bool waitForTimeSync(struct tm &timeInfo, unsigned long timeoutMs);
bool refreshCurrentTime();
void attemptTimeRecovery();
void resetTransientState();
void syncDisplayedScoreToCurrentTime();
void advanceDisplayedScore();

void startScreenFlash();
void updateScreenFlash();

void startExplosion(int x, int y);
void updateExplosion();
void drawExplosionFrameAt(int x, int y, uint16_t color);
void clearExplosionAt(int x, int y);

void drawPixelSafe(int x, int y, uint16_t color);
uint16_t getBackgroundColor(int px, int py);
void drawSpriteFromArray(int x, int y, uint16_t color, const uint8_t sprite[10][10]);
void drawScaledSprite(int x, int y, int w, int h, uint16_t color, const uint8_t sprite[10][10]);
void drawMissile(int x, int y);
void eraseMissile(int x, int y);
void drawInvaderMissileAt(int x, int y, uint16_t color);
void eraseInvaderMissile(int x, int y);

bool destroyOneInvaderAtMissile();
bool findInvaderAtMissile(int &hitRow, int &hitCol);
bool findLastInvaderToDestroy(int &invXCenter, int &invY);
bool anyInvadersAlive();
void fireMissileIfNeeded();
void fireMissile();
int getGapCenter(int gapIndex);
int chooseInvaderMissTargetX();
bool invaderMissileThreatensCannon(int missileBaseX, int missileY);
int getInvaderMissileDrawX(int missileBaseX, int missileY);
void startCannonDodge(int missileBaseX, int missileY);
void setNonDestructiveTarget();
void setDestructiveTarget();
void handleCannonMovement();
void drawScore();
void drawCannon();
void drawInvaders();
void drawScreen();
void initializeInvaders(int minutesElapsed);
void updateMissile();
void updateInvaderMissile();
void triggerUFO();
void updateUFO();
void drawUFOAt(int x, int y, uint16_t color);

//----------------------------------------
// Sprites
//----------------------------------------
const uint8_t cannonSprite[10][10] = {
  {0,0,0,0,1,1,0,0,0,0},
  {0,0,0,0,1,1,0,0,0,0},
  {0,0,0,1,1,1,1,0,0,0},
  {0,0,0,1,1,1,1,0,0,0},
  {0,1,1,1,1,1,1,1,1,0},
  {1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1}
};

const uint8_t ufoSprite[7][16] = {
  {0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0},
  {0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0},
  {0,0,1,1,0,1,0,1,1,0,1,0,1,1,0,0},
  {0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0},
  {1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1},
  {0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0},
  {0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0}
};

const uint8_t invaderSpriteRow0[10][10] = {
  {1,0,0,0,0,0,0,0,0,1},
  {0,1,1,0,0,0,0,0,1,1},
  {0,0,1,0,1,1,0,1,0,0},
  {0,0,0,1,1,1,1,0,0,0},
  {0,0,1,1,1,1,1,1,0,0},
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,1,1,1,1,1,1,0,1},
  {1,0,0,1,1,1,1,0,0,1},
  {0,1,1,0,0,0,0,1,1,0},
  {0,0,1,0,0,0,0,1,0,0}
};

const uint8_t invaderSpriteRow1[10][10] = {
  {0,0,0,0,1,1,0,0,0,0},
  {0,0,0,1,1,1,1,0,0,0},
  {0,0,1,1,1,1,1,1,0,0},
  {0,1,1,0,1,1,0,1,1,0},
  {1,1,1,0,1,1,0,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {0,0,0,1,0,0,1,0,0,0},
  {1,0,1,0,0,0,0,1,0,1},
  {0,1,0,1,1,1,1,0,1,0},
  {1,0,1,0,0,0,0,1,0,1}
};

const uint8_t invaderSpriteRow2[10][10] = {
  {0,0,1,1,1,1,1,1,0,0},
  {0,1,1,1,1,1,1,1,1,0},
  {1,1,0,1,1,1,1,0,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {0,1,1,1,1,1,1,1,1,0},
  {0,0,1,1,0,0,1,1,0,0},
  {0,1,1,0,0,0,0,1,1,0},
  {1,0,0,0,1,1,0,0,0,1},
  {1,0,1,0,0,0,0,1,0,1}
};

const uint8_t invaderSpriteRow3[10][10] = {
  {0,1,0,0,1,1,0,0,1,0},
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,1,1,1,1,1,1,0,1},
  {1,0,0,1,1,1,1,0,0,1},
  {0,1,1,1,1,1,1,1,1,0},
  {0,0,1,1,1,1,1,1,0,0},
  {0,0,1,1,0,0,1,1,0,0},
  {0,1,1,0,0,0,0,1,1,0},
  {1,1,0,0,0,0,0,0,1,1},
  {0,0,1,1,1,1,1,1,1,0}
};

const uint8_t invaderSpriteRow4[10][10] = {
  {0,0,0,1,1,1,1,0,0,0},
  {0,1,1,1,1,1,1,1,1,0},
  {1,1,1,0,1,1,0,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,1,1,1,1,1,1,0,1},
  {1,0,0,1,1,1,1,0,0,1},
  {0,1,1,0,1,1,0,1,1,0},
  {0,0,1,0,1,1,0,1,0,0},
  {0,1,1,0,0,0,0,1,1,0},
  {1,0,1,0,0,0,0,1,0,1}
};

const uint8_t invaderSpriteRow5[10][10] = {
  {1,1,1,1,1,1,1,1,1,1},
  {1,1,0,1,1,1,1,1,0,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,1,0,1,1,0,1,0,1},
  {0,1,0,1,1,1,1,0,1,0},
  {1,0,1,0,0,0,0,1,0,1},
  {1,1,1,1,1,1,1,1,1,1},
  {0,1,1,1,1,1,1,1,1,0},
  {0,0,1,1,1,1,1,1,0,0},
  {0,1,1,0,1,1,0,1,1,0}
};

const uint8_t invaderSpriteRow0B[10][10] = {
  {0,0,1,0,0,0,0,0,1,0},
  {0,0,0,1,0,0,0,1,0,0},
  {0,0,1,0,1,1,0,1,0,0},
  {0,0,0,1,1,1,1,0,0,0},
  {0,0,1,1,1,1,1,1,0,0},
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,1,1,1,1,1,1,0,1},
  {1,0,0,1,1,1,1,0,0,1},
  {1,1,0,0,0,0,0,0,1,1},
  {0,0,1,1,0,0,1,1,0,0}
};

const uint8_t invaderSpriteRow1B[10][10] = {
  {0,0,0,0,1,1,0,0,0,0},
  {0,0,0,1,1,1,1,0,0,0},
  {0,0,1,1,1,1,1,1,0,0},
  {0,1,1,0,1,1,0,1,1,0},
  {1,1,1,0,1,1,0,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,0,1,0,0,1,0,0,1},
  {0,1,0,0,1,1,0,0,1,0},
  {0,0,1,0,0,0,0,1,0,0},
  {0,1,0,0,0,0,0,0,1,0}
};

const uint8_t invaderSpriteRow2B[10][10] = {
  {0,0,1,1,1,1,1,1,0,0},
  {0,1,1,1,1,1,1,1,1,0},
  {1,1,0,1,1,1,1,0,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {0,1,1,1,1,1,1,1,1,0},
  {0,0,1,1,0,0,1,1,0,0},
  {0,0,1,0,0,0,0,1,0,0},
  {0,1,0,0,1,1,0,0,1,0},
  {1,0,0,0,0,0,0,0,0,1}
};

const uint8_t invaderSpriteRow3B[10][10] = {
  {0,1,0,0,1,1,0,0,1,0},
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,1,1,1,1,1,1,0,1},
  {1,0,0,1,1,1,1,0,0,1},
  {0,1,1,1,1,1,1,1,1,0},
  {0,0,1,1,1,1,1,1,0,0},
  {0,1,1,0,0,0,0,1,1,0},
  {1,0,0,1,0,0,1,0,0,1},
  {0,1,1,0,0,0,0,1,1,0},
  {1,0,0,1,1,1,1,0,0,1}
};

const uint8_t invaderSpriteRow4B[10][10] = {
  {0,0,0,1,1,1,1,0,0,0},
  {0,1,1,1,1,1,1,1,1,0},
  {1,1,1,0,1,1,0,1,1,1},
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,1,1,1,1,1,1,0,1},
  {1,0,0,1,1,1,1,0,0,1},
  {1,1,0,0,1,1,0,0,1,1},
  {0,0,1,0,0,0,0,1,0,0},
  {0,0,0,1,0,0,1,0,0,0},
  {0,0,1,0,0,0,0,1,0,0}
};

const uint8_t invaderSpriteRow5B[10][10] = {
  {1,1,1,1,1,1,1,1,1,1},
  {1,1,0,1,1,1,1,1,0,1},
  {1,1,1,1,1,1,1,1,1,1},
  {0,1,0,1,1,1,1,0,1,0},
  {1,0,1,0,1,1,0,1,0,1},
  {0,1,0,1,0,0,1,0,1,0},
  {1,1,1,1,1,1,1,1,1,1},
  {0,1,1,1,1,1,1,1,1,0},
  {0,1,1,0,1,1,0,1,1,0},
  {1,0,0,1,0,0,1,0,0,1}
};

const uint8_t explosionSprite[7][13] = {
  {0,0,1,0,0,0,1,0,0,0,1,0,0},
  {0,1,0,1,0,0,0,0,0,1,0,1,0},
  {1,0,0,0,1,0,0,0,1,0,0,0,1},
  {0,0,0,1,0,1,0,1,0,1,0,0,0},
  {1,0,0,0,1,0,0,0,1,0,0,0,1},
  {0,1,0,1,0,0,0,0,0,1,0,1,0},
  {0,0,1,0,0,0,1,0,0,0,1,0,0}
};

const uint8_t (*const invaderSpritesA[ROWS])[10] = {
  invaderSpriteRow0,
  invaderSpriteRow1,
  invaderSpriteRow2,
  invaderSpriteRow3,
  invaderSpriteRow4,
  invaderSpriteRow5
};

const uint8_t (*const invaderSpritesB[ROWS])[10] = {
  invaderSpriteRow0B,
  invaderSpriteRow1B,
  invaderSpriteRow2B,
  invaderSpriteRow3B,
  invaderSpriteRow4B,
  invaderSpriteRow5B
};

const uint16_t explosionColors[3] = {
  ILI9341_WHITE,
  ILI9341_YELLOW,
  0xFB60
};

//----------------------------------------
// Connectivity / Time
//----------------------------------------
bool connectWiFiWithTimeout(unsigned long timeoutMs) {
  if (ssid[0] == '\0') {
    Serial.println("WiFi credentials missing; starting unsynced.");
    return false;
  }

  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.persistent(false);
  WiFi.begin(ssid, password);

  Serial.print("Connecting WiFi");
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - start) < timeoutMs) {
    delay(250);
    Serial.print(".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("WiFi connected");
    return true;
  }

  Serial.println("WiFi timeout; starting unsynced.");
  return false;
}

bool waitForTimeSync(struct tm &timeInfo, unsigned long timeoutMs) {
  Serial.print("Waiting for NTP");
  unsigned long start = millis();
  while ((millis() - start) < timeoutMs) {
    if (getLocalTime(&timeInfo, 100)) {
      Serial.println("\nTime ready");
      return true;
    }
    delay(200);
    Serial.print(".");
  }
  Serial.println("\nTime sync timeout");
  return false;
}

bool refreshCurrentTime() {
  struct tm timeInfo;
  if (getLocalTime(&timeInfo, 10)) {
    currentTimeInfo = timeInfo;
    clockHasValidTime = true;
    return true;
  }
  return false;
}

void attemptTimeRecovery() {
  if (ssid[0] == '\0') return;

  unsigned long now = millis();
  if ((now - lastTimeRecoveryAttempt) < syncRetryInterval) return;
  lastTimeRecoveryAttempt = now;

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Retrying WiFi...");
    WiFi.disconnect();
    WiFi.begin(ssid, password);
    return;
  }

  configTzTime(tzInfo, ntpServer1, ntpServer2);
  struct tm timeInfo;
  if (waitForTimeSync(timeInfo, 1500)) {
    currentTimeInfo = timeInfo;
    clockHasValidTime = true;
    initializeInvaders(timeInfo.tm_min);
    lastMinute = timeInfo.tm_min;
    syncDisplayedScoreToCurrentTime();
    drawScreen();
  }
}

//----------------------------------------
// Effects
//----------------------------------------
void startScreenFlash() {
  flashActive = true;
  flashFrame = 0;
  lastFlashTick = millis();
  tft.fillScreen(ILI9341_WHITE);
}

void updateScreenFlash() {
  if (!flashActive) return;

  unsigned long now = millis();
  if ((now - lastFlashTick) < flashFrameInterval) return;

  lastFlashTick = now;
  flashFrame++;

  if (flashFrame >= 6) {
    flashActive = false;
    drawScreen();
    return;
  }

  tft.fillScreen((flashFrame % 2 == 0) ? ILI9341_WHITE : ILI9341_BLACK);
}

void startExplosion(int x, int y) {
  explosionX = x + 5 - 6;
  explosionY = y + 5 - 3;
  explosionFrame = 0;
  explosionActive = true;
  lastExplosionTick = millis();
  drawExplosionFrameAt(explosionX, explosionY, explosionColors[explosionFrame]);
}

void updateExplosion() {
  if (!explosionActive) return;

  unsigned long now = millis();
  if ((now - lastExplosionTick) < explosionFrameInterval) return;

  clearExplosionAt(explosionX, explosionY);
  lastExplosionTick = now;
  explosionFrame++;

  if (explosionFrame >= 3) {
    explosionActive = false;
    explosionX = -1;
    explosionY = -1;
    return;
  }

  drawExplosionFrameAt(explosionX, explosionY, explosionColors[explosionFrame]);
}

void resetTransientState() {
  unsigned long now = millis();

  missileX = -1;
  missileY = -1;
  missileImpactPending = false;
  invMissileX = -1;
  invMissileY = -1;
  invMissileTargetX = -1;
  targetX = -1;
  topMinuteFire = false;
  cannonAligning = false;
  cannonDodging = false;
  dodgeTargetX = -1;
  pendingDestructiveShot = false;
  pendingScoreUpdates = 0;
  endOfWavePending = false;
  endOfWaveResetAt = 0;

  explosionActive = false;
  explosionX = -1;
  explosionY = -1;

  flashActive = false;
  flashFrame = 0;

  lastInvaderShot = now;
  lastNonDestructiveAttempt = now;
}

void syncDisplayedScoreToCurrentTime() {
  displayedScoreHour = currentTimeInfo.tm_hour;
  displayedScoreMinute = currentTimeInfo.tm_min;
  scoreInitialized = true;
  pendingScoreUpdates = 0;
}

void advanceDisplayedScore() {
  if (!scoreInitialized) {
    syncDisplayedScoreToCurrentTime();
    return;
  }

  displayedScoreMinute++;
  if (displayedScoreMinute >= 60) {
    displayedScoreMinute = 0;
    displayedScoreHour = (displayedScoreHour + 1) % 24;
  }
}

//----------------------------------------
// UFO
//----------------------------------------
void triggerUFO() {
  if (!ufoActive) {
    ufoX = -UFO_WIDTH;
    ufoActive = true;
  }
}

void drawUFOAt(int x, int y, uint16_t color) {
  for (int row = 0; row < UFO_HEIGHT; row++) {
    for (int col = 0; col < UFO_WIDTH; col++) {
      if (ufoSprite[row][col]) {
        drawPixelSafe(x + col, y + row, color);
      }
    }
  }
}

void updateUFO() {
  if (!ufoActive) return;

  drawUFOAt(ufoX, ufoY, ILI9341_BLACK);
  ufoX += 1;

  if (ufoX > SCREEN_WIDTH) {
    ufoActive = false;
    ufoX = -1;
    return;
  }

  drawUFOAt(ufoX, ufoY, ILI9341_MAGENTA);
}

//----------------------------------------
// Setup / Loop
//----------------------------------------
void setup() {
  Serial.begin(115200);
  randomSeed(micros());

  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);

  SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI, TFT_CS);
  tft.begin();
  tft.setRotation(0);

  initializeInvaders(0);
  drawScreen();

  if (connectWiFiWithTimeout(wifiConnectTimeout)) {
    configTzTime(tzInfo, ntpServer1, ntpServer2);

    struct tm timeInfo;
    if (waitForTimeSync(timeInfo, ntpSyncTimeout)) {
      currentTimeInfo = timeInfo;
      clockHasValidTime = true;
      initializeInvaders(timeInfo.tm_min);
      lastMinute = timeInfo.tm_min;
      syncDisplayedScoreToCurrentTime();
    }
  }

  drawScreen();
}

void loop() {
  refreshCurrentTime();
  if (!clockHasValidTime) attemptTimeRecovery();

  if (clockHasValidTime) {
    if (lastMinute == -1) {
      initializeInvaders(currentTimeInfo.tm_min);
      lastMinute = currentTimeInfo.tm_min;
      syncDisplayedScoreToCurrentTime();
      drawScreen();
    } else if (currentTimeInfo.tm_min != lastMinute) {
      lastMinute = currentTimeInfo.tm_min;
      pendingScoreUpdates++;
      pendingDestructiveShot = true;

      if (currentTimeInfo.tm_min == 0) {
        endOfWavePending = true;
      } else {
        if ((currentTimeInfo.tm_min % 15) == 0) triggerUFO();
      }
    }
  }

  if (flashActive) {
    updateScreenFlash();
    delay(30);
    return;
  }

  unsigned long now = millis();

  if (endOfWaveResetAt != 0 && now >= endOfWaveResetAt) {
    resetTransientState();
    initializeInvaders(0);
    startScreenFlash();
    triggerUFO();
    delay(30);
    return;
  }

  if (pendingDestructiveShot && endOfWaveResetAt == 0 &&
      !topMinuteFire && !cannonAligning && !cannonDodging && missileY == -1) {
    setDestructiveTarget();
  } else if (!pendingDestructiveShot && !endOfWavePending && endOfWaveResetAt == 0 &&
             !topMinuteFire && !cannonAligning && !cannonDodging && missileY == -1) {
    if ((now - lastNonDestructiveAttempt) >= nonDestructiveInterval) {
      setNonDestructiveTarget();
      lastNonDestructiveAttempt = now;
    }
  }

  if ((now - lastFrameSwitch) >= frameSwitchInterval) {
    lastFrameSwitch = now;
    invaderFrame = !invaderFrame;
    drawInvaders();
    if (missileY > -1) drawMissile(missileX, missileY);
    if (invMissileY > -1) drawInvaderMissileAt(invMissileX, invMissileY, ILI9341_CYAN);
    if (explosionActive) drawExplosionFrameAt(explosionX, explosionY, explosionColors[explosionFrame]);
  }

  handleCannonMovement();
  updateMissile();
  updateInvaderMissile();
  updateExplosion();
  updateUFO();

  delay(30);
}

//----------------------------------------
// Game Logic
//----------------------------------------
void initializeInvaders(int minutesElapsed) {
  minutesElapsed = constrain(minutesElapsed, 0, ROWS * COLS);

  for (int r = 0; r < ROWS; r++) {
    for (int c = 0; c < COLS; c++) {
      invaders[r][c] = true;
    }
  }

  int destroyed = 0;
  for (int row = ROWS - 1; row >= 0 && destroyed < minutesElapsed; row--) {
    for (int col = COLS - 1; col >= 0 && destroyed < minutesElapsed; col--) {
      invaders[row][col] = false;
      destroyed++;
    }
  }
}

void setDestructiveTarget() {
  int invXCenter;
  int invY;

  if (findLastInvaderToDestroy(invXCenter, invY)) {
    targetX = invXCenter;
    topMinuteFire = true;
    cannonAligning = true;
    pendingDestructiveShot = false;
  } else {
    pendingDestructiveShot = false;
    if (endOfWavePending && endOfWaveResetAt == 0) {
      endOfWaveResetAt = millis() + endOfWaveResetDelay;
    }
  }
}

int getGapCenter(int gapIndex) {
  return INVADER_LEFT_MARGIN + INVADER_WIDTH + (INVADER_X_SPACING / 2) +
         gapIndex * (INVADER_WIDTH + INVADER_X_SPACING);
}

int chooseInvaderMissTargetX() {
  int cannonCenter = cannonX + (BASE_WIDTH / 2);
  return constrain(cannonCenter + random(-4, 5), invaderMissMinX, invaderMissMaxX);
}

int getInvaderMissileDrawX(int missileBaseX, int missileY) {
  bool phase = (missileY / 4) % 2;
  return phase ? missileBaseX + 2 : missileBaseX - 2;
}

bool invaderMissileThreatensCannon(int missileBaseX, int missileY) {
  int cannonTop = SCREEN_HEIGHT - BASE_HEIGHT - 10;
  if (missileY < (cannonTop - invaderMissRedirectZone)) return false;

  int drawX = getInvaderMissileDrawX(missileBaseX, missileY);
  int safeLeft = cannonX - invaderMissSafetyMargin;
  int safeRight = cannonX + BASE_WIDTH + invaderMissSafetyMargin;

  return drawX >= safeLeft && drawX <= safeRight;
}

void startCannonDodge(int missileBaseX, int missileY) {
  int drawX = getInvaderMissileDrawX(missileBaseX, missileY);
  int leftTarget = constrain(drawX - BASE_WIDTH - invaderMissSafetyMargin - 2,
                             0, SCREEN_WIDTH - BASE_WIDTH);
  int rightTarget = constrain(drawX + invaderMissSafetyMargin + 2,
                              0, SCREEN_WIDTH - BASE_WIDTH);

  bool canGoLeft = (leftTarget + BASE_WIDTH) < (drawX - 1);
  bool canGoRight = rightTarget > (drawX + 1);

  if (canGoLeft && canGoRight) {
    dodgeTargetX = (abs(cannonX - leftTarget) <= abs(cannonX - rightTarget)) ? leftTarget : rightTarget;
  } else if (canGoLeft) {
    dodgeTargetX = leftTarget;
  } else if (canGoRight) {
    dodgeTargetX = rightTarget;
  } else {
    dodgeTargetX = (drawX < (SCREEN_WIDTH / 2)) ? (SCREEN_WIDTH - BASE_WIDTH) : 0;
  }

  cannonDodging = true;
}

void setNonDestructiveTarget() {
  targetX = getGapCenter(random(0, COLS - 1));
  topMinuteFire = false;
  cannonAligning = true;
}

bool findLastInvaderToDestroy(int &invXCenter, int &invY) {
  for (int row = ROWS - 1; row >= 0; row--) {
    for (int col = COLS - 1; col >= 0; col--) {
      if (invaders[row][col]) {
        int invX = INVADER_LEFT_MARGIN + col * (INVADER_WIDTH + INVADER_X_SPACING);
        int invYPos = INVADER_TOP_MARGIN + row * (INVADER_HEIGHT + INVADER_Y_SPACING);
        invXCenter = invX + (INVADER_WIDTH / 2);
        invY = invYPos;
        return true;
      }
    }
  }
  return false;
}

bool anyInvadersAlive() {
  for (int row = 0; row < ROWS; row++) {
    for (int col = 0; col < COLS; col++) {
      if (invaders[row][col]) return true;
    }
  }
  return false;
}

void fireMissileIfNeeded() {
  if (missileY == -1) fireMissile();
}

void fireMissile() {
  missileX = cannonX + (BASE_WIDTH / 2);
  missileY = SCREEN_HEIGHT - BASE_HEIGHT - MISSILE_HEIGHT - 10;
  drawMissile(missileX, missileY);
}

void updateMissile() {
  if (missileY == -1) return;

  if (missileImpactPending) {
    eraseMissile(missileX, missileY);
    if (!destroyOneInvaderAtMissile() && topMinuteFire) {
      pendingDestructiveShot = (pendingScoreUpdates > 0);
    }
    missileX = -1;
    missileY = -1;
    missileImpactPending = false;
    topMinuteFire = false;
    return;
  }

  eraseMissile(missileX, missileY);
  missileY -= 3;

  if (missileY <= 46) {
    missileX = -1;
    missileY = -1;
    missileImpactPending = false;
    if (topMinuteFire) {
      pendingDestructiveShot = (pendingScoreUpdates > 0);
    }
    topMinuteFire = false;
    return;
  }

  drawMissile(missileX, missileY);

  if (topMinuteFire) {
    int hitRow;
    int hitCol;
    if (findInvaderAtMissile(hitRow, hitCol)) {
      missileImpactPending = true;
    }
  }
}

void updateInvaderMissile() {
  unsigned long now = millis();

  if (invMissileY == -1 && (now - lastInvaderShot) >= invaderShotInterval) {
    int aliveCount = 0;
    for (int r = 0; r < ROWS; r++) {
      for (int c = 0; c < COLS; c++) {
        if (invaders[r][c]) aliveCount++;
      }
    }

    if (aliveCount > 0) {
      int pick = random(0, aliveCount);
      int idx = 0;
      bool found = false;

      for (int r = 0; r < ROWS && !found; r++) {
        for (int c = 0; c < COLS; c++) {
          if (!invaders[r][c]) continue;
          if (idx == pick) {
            invMissileX = INVADER_LEFT_MARGIN + c * (INVADER_WIDTH + INVADER_X_SPACING) +
                          (INVADER_WIDTH / 2);
            invMissileY = INVADER_TOP_MARGIN + r * (INVADER_HEIGHT + INVADER_Y_SPACING) +
                          INVADER_HEIGHT;
            invMissileTargetX = chooseInvaderMissTargetX();
            lastInvaderShot = now;
            found = true;
            break;
          }
          idx++;
        }
      }
    }
  }

  if (invMissileY == -1) return;

  eraseInvaderMissile(invMissileX, invMissileY);

  invMissileY += 2;
  if (invMissileX < invMissileTargetX) invMissileX++;
  else if (invMissileX > invMissileTargetX) invMissileX--;

  if (invaderMissileThreatensCannon(invMissileX, invMissileY)) {
    startCannonDodge(invMissileX, invMissileY);
  }

  if (invMissileY >= SCREEN_HEIGHT) {
    eraseInvaderMissile(invMissileX, invMissileY);
    invMissileX = -1;
    invMissileY = -1;
    invMissileTargetX = -1;
    return;
  }

  drawInvaderMissileAt(invMissileX, invMissileY, ILI9341_CYAN);
}

bool findInvaderAtMissile(int &hitRow, int &hitCol) {
  // Use only the leading tip of the missile so the visible impact lines up
  // with the hit event and score update.
  const int missileTipHeight = 2;
  int mLeft = missileX - (MISSILE_WIDTH / 2);
  int mRight = mLeft + MISSILE_WIDTH - 1;
  int mTop = missileY;
  int mBottom = missileY + missileTipHeight - 1;

  for (int row = 0; row < ROWS; row++) {
    for (int col = 0; col < COLS; col++) {
      if (!invaders[row][col]) continue;

      int iX = INVADER_LEFT_MARGIN + col * (INVADER_WIDTH + INVADER_X_SPACING);
      int iY = INVADER_TOP_MARGIN + row * (INVADER_HEIGHT + INVADER_Y_SPACING);
      int iX2 = iX + INVADER_WIDTH - 1;
      int iY2 = iY + INVADER_HEIGHT - 1;

      if (!(iX2 < mLeft || iX > mRight || iY2 < mTop || iY > mBottom)) {
        hitRow = row;
        hitCol = col;
        return true;
      }
    }
  }
  return false;
}

bool destroyOneInvaderAtMissile() {
  int hitRow;
  int hitCol;
  if (!findInvaderAtMissile(hitRow, hitCol)) return false;

  int iX = INVADER_LEFT_MARGIN + hitCol * (INVADER_WIDTH + INVADER_X_SPACING);
  int iY = INVADER_TOP_MARGIN + hitRow * (INVADER_HEIGHT + INVADER_Y_SPACING);

  invaders[hitRow][hitCol] = false;
  startExplosion(iX, iY);

  if (pendingScoreUpdates > 0) {
    pendingScoreUpdates--;
    advanceDisplayedScore();
    drawScore();
  }

  if (endOfWavePending) {
    if (anyInvadersAlive()) {
      pendingDestructiveShot = (pendingScoreUpdates > 0);
    } else if (endOfWaveResetAt == 0) {
      endOfWaveResetAt = millis() + endOfWaveResetDelay;
    }
  } else {
    pendingDestructiveShot = (pendingScoreUpdates > 0);
  }
...

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

Credits

Brian Wente
7 projects • 8 followers

Comments