// SPACE INVADERS V4 // XIAO RP2040 + SSD1306 OLED + touch pads // Updated with centered title, intro art, denser/higher invaders, // level complete screen, game over rules, and improved sprite shapes. #include #include #include U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); static const uint8_t PIN_LEFT = D1; static const uint8_t PIN_RIGHT = D7; static const uint8_t PIN_A = D9; static const uint8_t PIN_B = D10; #define N_TOUCH 4 #define THRESHOLD 6 int touch_pins[N_TOUCH] = { PIN_LEFT, PIN_RIGHT, PIN_A, PIN_B }; bool nowT[N_TOUCH] = {false, false, false, false}; bool pastT[N_TOUCH] = {false, false, false, false}; void update_touch() { for (int i = 0; i < N_TOUCH; i++) { int p = touch_pins[i]; pinMode(p, OUTPUT); digitalWrite(p, LOW); delayMicroseconds(20); pinMode(p, INPUT_PULLUP); int t = 0; while (!digitalRead(p) && t < 200) t++; pastT[i] = nowT[i]; nowT[i] = (t > THRESHOLD); } } bool pressed(int i) { return nowT[i] && !pastT[i]; } bool held(int i) { return nowT[i]; } enum { T_LEFT = 0, T_RIGHT = 1, T_A = 2, T_B = 3 }; // ================= GAME CONSTANTS ================= #define EC 5 #define ER 3 #define MAX_BULLETS 4 const int PLAYER_Y = 58; const int PLAYER_W = 12; const int ENEMY_W = 10; const int ENEMY_H = 8; const int ENEMY_GAP_X = 5; const int ENEMY_GAP_Y = 4; // denser vertically const int ENEMY_START_X = 18; const int ENEMY_START_Y = 10; // higher // ================= GAME STATE ================= int px = 58; int level = 1; int ex = ENEMY_START_X; int ey = ENEMY_START_Y; int dir = 1; int enemyStepDelay = 22; int enemyFrame = 0; unsigned long lastEnemyMove = 0; bool enemy[ER][EC]; bool intro = true; bool gameOver = false; bool levelComplete = false; int completedLevel = 1; unsigned long introTick = 0; struct Bullet { int x; int y; bool a; }; Bullet bullets[MAX_BULLETS]; // ================= HELPERS ================= void clearBullets() { for (int i = 0; i < MAX_BULLETS; i++) bullets[i].a = false; } void initEnemies() { for (int r = 0; r < ER; r++) { for (int c = 0; c < EC; c++) { enemy[r][c] = true; } } ex = ENEMY_START_X; ey = ENEMY_START_Y; dir = 1; clearBullets(); } bool enemiesAlive() { for (int r = 0; r < ER; r++) { for (int c = 0; c < EC; c++) { if (enemy[r][c]) return true; } } return false; } void startGame() { level = 1; px = 58; gameOver = false; levelComplete = false; initEnemies(); } void startNextLevel() { level++; if (level > 3) level = 1; levelComplete = false; px = 58; initEnemies(); } // ================= PLAYER ================= void drawPlayer() { // more interesting ship shape u8g2.drawPixel(px + 5, PLAYER_Y - 3); u8g2.drawBox(px + 4, PLAYER_Y - 2, 3, 1); u8g2.drawBox(px + 3, PLAYER_Y - 1, 5, 1); u8g2.drawBox(px + 2, PLAYER_Y, 7, 1); u8g2.drawBox(px + 1, PLAYER_Y + 1, 9, 1); u8g2.drawBox(px, PLAYER_Y + 2, 12, 1); u8g2.drawPixel(px + 1, PLAYER_Y + 3); u8g2.drawPixel(px + 10, PLAYER_Y + 3); } // ================= BULLETS ================= void fire() { for (int i = 0; i < MAX_BULLETS; i++) { if (!bullets[i].a) { bullets[i].x = px + PLAYER_W / 2; bullets[i].y = PLAYER_Y - 5; bullets[i].a = true; break; } } } void updateBullets() { for (int i = 0; i < MAX_BULLETS; i++) { if (bullets[i].a) { bullets[i].y -= 3; if (bullets[i].y < 0) bullets[i].a = false; } } } void drawBullets() { for (int i = 0; i < MAX_BULLETS; i++) { if (bullets[i].a) u8g2.drawVLine(bullets[i].x, bullets[i].y, 3); } } // ================= ENEMIES ================= void drawEnemyType1(int x, int y, bool phase) { u8g2.drawBox(x + 2, y, 6, 1); u8g2.drawBox(x + 1, y + 1, 8, 1); u8g2.drawBox(x, y + 2, 10, 1); u8g2.drawPixel(x + 2, y + 3); u8g2.drawPixel(x + 7, y + 3); if (phase) { u8g2.drawLine(x + 1, y + 4, x + 3, y + 6); u8g2.drawLine(x + 8, y + 4, x + 6, y + 6); } else { u8g2.drawLine(x + 2, y + 4, x + 1, y + 6); u8g2.drawLine(x + 7, y + 4, x + 8, y + 6); } } void drawEnemyType2(int x, int y, bool phase) { u8g2.drawFrame(x + 1, y + 1, 8, 4); u8g2.drawPixel(x, y + 2); u8g2.drawPixel(x + 9, y + 2); u8g2.drawPixel(x + 3, y + 2); u8g2.drawPixel(x + 6, y + 2); if (phase) { u8g2.drawLine(x + 2, y + 5, x + 1, y + 6); u8g2.drawLine(x + 7, y + 5, x + 8, y + 6); } else { u8g2.drawLine(x + 3, y + 5, x + 3, y + 6); u8g2.drawLine(x + 6, y + 5, x + 6, y + 6); } } void drawEnemyType3(int x, int y, bool phase) { u8g2.drawPixel(x + 4, y); u8g2.drawBox(x + 2, y + 1, 5, 1); u8g2.drawBox(x + 1, y + 2, 7, 1); u8g2.drawBox(x, y + 3, 9, 1); u8g2.drawPixel(x + 2, y + 4); u8g2.drawPixel(x + 6, y + 4); if (phase) { u8g2.drawLine(x + 2, y + 5, x + 1, y + 6); u8g2.drawLine(x + 6, y + 5, x + 7, y + 6); } else { u8g2.drawLine(x + 3, y + 5, x + 3, y + 6); u8g2.drawLine(x + 5, y + 5, x + 5, y + 6); } } void drawEnemy(int x, int y, int rowType) { bool phase = ((millis() / 220) % 2) == 0; if (level == 1) { drawEnemyType1(x, y, phase); } else if (level == 2) { drawEnemyType2(x, y, phase); } else { // mix on level 3 if (rowType == 0) drawEnemyType1(x, y, phase); else if (rowType == 1) drawEnemyType2(x, y, phase); else drawEnemyType3(x, y, phase); } } void drawEnemies() { for (int r = 0; r < ER; r++) { for (int c = 0; c < EC; c++) { if (enemy[r][c]) { int x = ex + c * (ENEMY_W + ENEMY_GAP_X); int y = ey + r * (ENEMY_H + ENEMY_GAP_Y); drawEnemy(x, y, r); } } } } void updateEnemies() { if (millis() - lastEnemyMove < (unsigned long)enemyStepDelay) return; lastEnemyMove = millis(); enemyFrame++; int speed = level; ex += dir * speed; // Find bounds using only alive invaders int leftMost = 999; int rightMost = -999; int bottomMost = -999; for (int r = 0; r < ER; r++) { for (int c = 0; c < EC; c++) { if (!enemy[r][c]) continue; int x = ex + c * (ENEMY_W + ENEMY_GAP_X); int y = ey + r * (ENEMY_H + ENEMY_GAP_Y); if (x < leftMost) leftMost = x; if (x + ENEMY_W > rightMost) rightMost = x + ENEMY_W; if (y + ENEMY_H > bottomMost) bottomMost = y + ENEMY_H; } } if (leftMost == 999) return; // no alive invaders if (leftMost < 2 || rightMost > 126) { dir = -dir; ex += dir * speed; ey += 3; // recompute bottom after stepping down bottomMost = -999; for (int r = 0; r < ER; r++) { for (int c = 0; c < EC; c++) { if (!enemy[r][c]) continue; int y = ey + r * (ENEMY_H + ENEMY_GAP_Y); if (y + ENEMY_H > bottomMost) bottomMost = y + ENEMY_H; } } } // Game over only if a STILL-ALIVE invader reaches the base height if (bottomMost >= PLAYER_Y - 2) { gameOver = true; } // Game over only if a STILL-ALIVE invader touches the player area for (int r = 0; r < ER; r++) { for (int c = 0; c < EC; c++) { if (!enemy[r][c]) continue; int x = ex + c * (ENEMY_W + ENEMY_GAP_X); int y = ey + r * (ENEMY_H + ENEMY_GAP_Y); bool hitPlayerZone = !(x + ENEMY_W < px || x > px + PLAYER_W || y + ENEMY_H < PLAYER_Y - 4 || y > PLAYER_Y + 3); if (hitPlayerZone) { gameOver = true; } } } } // ================= COLLISION ================= void checkHit() { for (int i = 0; i < MAX_BULLETS; i++) { if (!bullets[i].a) continue; for (int r = 0; r < ER; r++) { for (int c = 0; c < EC; c++) { if (!enemy[r][c]) continue; int x = ex + c * (ENEMY_W + ENEMY_GAP_X); int y = ey + r * (ENEMY_H + ENEMY_GAP_Y); if (bullets[i].x > x && bullets[i].x < x + ENEMY_W && bullets[i].y > y && bullets[i].y < y + ENEMY_H) { enemy[r][c] = false; bullets[i].a = false; } } } } } // ================= INTRO ================= void drawMonsterHead3D(int x, int y, int frame) { // pseudo-3D face on right side u8g2.drawRFrame(x, y, 28, 20, 3); u8g2.drawRFrame(x + 2, y + 2, 24, 16, 2); u8g2.drawDisc(x + 8, y + 8, 2, U8G2_DRAW_ALL); u8g2.drawDisc(x + 20, y + 8, 2, U8G2_DRAW_ALL); u8g2.drawLine(x + 6, y + 14, x + 22, y + 14); u8g2.drawLine(x + 3, y + 3, x - 2, y - 2); u8g2.drawLine(x + 25, y + 3, x + 30, y - 2); if (frame % 20 < 10) { u8g2.drawLine(x + 10, y + 15, x + 14, y + 17); u8g2.drawLine(x + 18, y + 15, x + 14, y + 17); } else { u8g2.drawLine(x + 9, y + 16, x + 19, y + 16); } } void drawIntro() { u8g2.clearBuffer(); int frame = (millis() / 90) % 40; // left side text u8g2.setFont(u8g2_font_7x13B_tr); u8g2.drawStr(8, 14, "SPACE"); u8g2.drawStr(8, 28, "INVADERS"); u8g2.setFont(u8g2_font_5x8_tf); u8g2.drawStr(8, 39, "by Yaro"); // right side monster face drawMonsterHead3D(88, 8, frame); // small battle scene lower area int invX = 16 + (frame % 18); drawEnemyType3(invX, 46, frame % 2 == 0); drawEnemyType2(invX + 18, 46, frame % 2 != 0); drawPlayer(); if (frame % 16 < 8) { u8g2.drawVLine(px + 6, 44, 8); } u8g2.drawHLine(0, 54, 128); u8g2.setFont(u8g2_font_6x12_tr); u8g2.drawStr(28, 63, "PRESS B"); u8g2.sendBuffer(); } // ================= SCREENS ================= void drawHUD() { u8g2.setFont(u8g2_font_5x8_tf); u8g2.setCursor(0, 8); u8g2.print("LVL:"); u8g2.print(level); } void drawLevelComplete() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_6x12_tr); u8g2.drawStr(14, 20, "LEVEL COMPLETE"); u8g2.setFont(u8g2_font_5x8_tf); u8g2.drawStr(22, 34, "Press B for the"); u8g2.drawStr(32, 44, "next level"); u8g2.drawStr(48, 56, "L="); u8g2.print(completedLevel); u8g2.sendBuffer(); } void drawGameOver() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_7x13B_tr); u8g2.drawStr(22, 22, "GAME OVER"); u8g2.setFont(u8g2_font_5x8_tf); u8g2.drawStr(18, 38, "Invaders reached"); u8g2.drawStr(35, 46, "the base"); u8g2.drawStr(18, 58, "Press B to restart"); u8g2.sendBuffer(); } void render() { u8g2.clearBuffer(); drawHUD(); drawEnemies(); drawBullets(); drawPlayer(); u8g2.sendBuffer(); } // ================= SETUP ================= void setup() { Serial.begin(115200); Wire.begin(); u8g2.begin(); startGame(); } // ================= LOOP ================= void loop() { update_touch(); if (intro) { drawIntro(); if (pressed(T_B)) { intro = false; startGame(); } delay(30); return; } if (gameOver) { drawGameOver(); if (pressed(T_B)) { startGame(); } delay(30); return; } if (levelComplete) { drawLevelComplete(); if (pressed(T_B)) { startNextLevel(); } delay(30); return; } if (held(T_LEFT)) px -= 2; if (held(T_RIGHT)) px += 2; if (px < 0) px = 0; if (px > 116) px = 116; if (pressed(T_A)) fire(); updateBullets(); updateEnemies(); checkHit(); if (!enemiesAlive()) { completedLevel = level; levelComplete = true; } render(); delay(30); }