Arduino Uno - Space Invaders

Typ_projektu
Arduino
Zdjecie główne
Krótki opis projektu

   W ramach projektu stworzono grę inspirowaną serią Space Invaders, którą wgrano na moduł Arduino Uno. Grę napisano w języku C++ przy wsparciu technologii AI (Chat GPT). Cały układ, natomiast, został utworzony z powyższej płytki, modułu Game Console v0.2 beta, wyświetlacza.

Niezbędne elementy

1. Płytka Arduino UNO

2. moduł SIC Game Console v0.2 beta

3. Wyświetlacz IIC OLED SSD1306

Opis projektu

   W ramach projektu stworzono grę inspirowaną serią Space Invaders, którą wgrano na moduł Arduino Uno. Grę napisano w języku C++ przy wsparciu technologii AI (Chat GPT). Cały układ, natomiast, został utworzony z powyższej płytki, modułu Game Console v0.2 beta, wyświetlacza.

   Gracz może poruszać się na prawo i lewo oraz strzelać. Początkowo mamy jednego przeciwnika, który wystrzeliwuje w kierunku gracza pociski. Po wyeliminowaniu go pojawia się dwóch przeciwników, a z kolei po wyeliminowaniu tych dwóch, pojawia się trzech, a na końcu pojawia się boss. Potem cykl pojawiania się przeciwników działa w zapętleniu. Gracz może zostać trafiony 3 razy. Następnie, na ekranie wyświetla się napis „GAME OVER”, z wynikiem gracza, który jest liczony w czasie trwania rozgrywki. Po przegranej gra resetuje się po chwili, a następnie gracz zaczyna od fazy, w której zginął z wyzerowanym licznikiem punktów.

   Wszyscy standardowi przeciwnicy pojawiają się na jednej wysokości. Każdy, jednak, ma różne tempo strzałów. Jedynie boss pojawia się bliżej środka ekranu. W początkowej wersji programu przeciwnicy jak i gracz mieli formę prostokątów, ale, aby gra była bardziej przyjazna wizualnie dla użytkownika, zdecydowano się na zastąpienie tak prostych form geometrycznych kształtami bardziej przypominającymi statki kosmiczne. Oczywiście, gracz, przeciwnicy jak i sam boss mają oddzielnie wyglądające modele graficzne.

Cechy gry: 

  1. Prosta rozgrywka: Gracz kontroluje statek kosmiczny, który może poruszać się w lewo i w prawo w dolnej części ekranu. Celem jest zestrzelenie fal kosmitów.

  2. Wynik: Gracz zdobywa punkty za każdego zestrzelonego kosmitę. Wynik wyświetla się na końcu rozgrywki.

  3. Poziomy trudności: Gra staje się trudniejsza w miarę postępów. Pojawia się coraz więcej kosmitów z różną częstotliwością strzałów.

  4. Strategia i zręczność: Sukces w grze wymaga zarówno strategicznego planowania, jak i zręczności w manewrowaniu i strzelaniu.

Zdjęcia
kod programu
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

#define INVADER_SIZE 6
#define PLAYER_WIDTH 10
#define PLAYER_HEIGHT 8
#define BULLET_SIZE 2

#define OLED_RESET 4
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define BUTTON_LEFT 5
#define BUTTON_RIGHT 4
#define BUTTON_FIRE 7
#define MAX_INVADER_BULLETS 3 // Maksymalnie 3 pociski, po jednym dla każdego przeciwnika
#define MAX_PLAYER_BULLETS 2

#define INVADER_SHOOT_INTERVAL 1000 // Każdy przeciwnik strzela raz na sekundę
#define MIN_INVADER_SPACING 10  // Minimalny odstęp między przeciwnikami

#define BOSS_WIDTH 24
#define BOSS_HEIGHT 16
#define BOSS_HIT_THRESHOLD 10

#define BOSS_BULLET_SPEED 2
#define BOSS_BULLET_WAVE_AMPLITUDE 5
#define BOSS_BULLET_WAVE_FREQUENCY 0.1
#define MAX_BOSS_BULLETS 10

#define BOSS_BULLET_WAVE_LENGTH 5

bool gameOver = false;

int bossBulletPositionX[MAX_BOSS_BULLETS];
int bossBulletPositionY[MAX_BOSS_BULLETS];

int bossBulletPositionX2[MAX_BOSS_BULLETS];
int bossBulletPositionY2[MAX_BOSS_BULLETS];
bool bossBulletActive[MAX_BOSS_BULLETS];
int bossBulletTimeSinceLastShot = 0; // Timer do zarządzania czasem między strzałami bossa


int bossBulletDirectionX[MAX_BOSS_BULLETS]; // Dla ataku falowego
int bossBulletDirectionY[MAX_BOSS_BULLETS]; // Dla ataku falowego

int bossBulletDirectionX2[MAX_BOSS_BULLETS]; // Dla ataku falowego
int bossBulletDirectionY2[MAX_BOSS_BULLETS];

unsigned long lastBossAttackTime = 0; // Kiedy ostatni atak został wykonany
int bossAttackPattern = 0; // Który atak ma być wykonany

bool bossActive = false;
int bossHits = 0;
int bossPositionX = (SCREEN_WIDTH - BOSS_WIDTH) / 2;
int bossPositionY = 10;


int invaderBulletPositionX[MAX_INVADER_BULLETS];
int invaderBulletPositionY[MAX_INVADER_BULLETS];
bool invaderBulletActive[MAX_INVADER_BULLETS] = {false};
unsigned long invaderLastShot[MAX_INVADER_BULLETS] = {0}; // Czas ostatniego strzału dla każdego przeciwnika

int playerPositionX = (SCREEN_WIDTH - PLAYER_WIDTH) / 2;
int playerPositionY = SCREEN_HEIGHT - PLAYER_HEIGHT - 2;

const int MAX_INVADERS = 3; // Maksymalnie 3 przeciwników
int invaderPositionX[MAX_INVADERS];
int invaderPositionY[MAX_INVADERS];
bool invaderActive[MAX_INVADERS] = {false};

int playerBulletPositionX[MAX_PLAYER_BULLETS];
int playerBulletPositionY[MAX_PLAYER_BULLETS];
bool playerBulletActive[MAX_PLAYER_BULLETS] = {false};

int playerLives = 3;
bool hitFlash = false;

bool gameActive = true;
int score = 0;
int currentPhase = 1; // Obecna faza gry

void setup() {
  pinMode(BUTTON_LEFT, INPUT_PULLUP);
  pinMode(BUTTON_RIGHT, INPUT_PULLUP);
  pinMode(BUTTON_FIRE, INPUT_PULLUP);

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setRotation(2);
  display.clearDisplay();
  display.display();

  resetInvaders(currentPhase);
}

void resetInvaders(int count) {
  int lastInvaderX = -MIN_INVADER_SPACING; // Start poza ekranem

   if (count == 4) { // Czwarta faza z bossem
    bossActive = true;
    bossHits = 0;
    // Możesz tutaj zresetować pozycje i aktywność zwykłych przeciwników, jeśli jest to potrzebne
    for (int i = 0; i < MAX_INVADERS; i++) {
        invaderActive[i] = false;
    }
}
if(count != 4){
  for (int i = 0; i < count; i++) {
    int invaderX;
    do {
      invaderX = random(0, SCREEN_WIDTH - INVADER_SIZE);
    } while (abs(invaderX - lastInvaderX) < MIN_INVADER_SPACING); // Zapewnienie odstępu
    lastInvaderX = invaderX;

    invaderActive[i] = true;
    invaderPositionX[i] = invaderX;
    invaderPositionY[i] = 5;
  }
}

}



void drawPlayer() {
  // Kadłub
  display.fillTriangle(playerPositionX + PLAYER_WIDTH / 2, playerPositionY, playerPositionX, playerPositionY + PLAYER_HEIGHT, playerPositionX + PLAYER_WIDTH, playerPositionY + PLAYER_HEIGHT, WHITE);
  // Skrzydła
  display.fillTriangle(playerPositionX, playerPositionY + PLAYER_HEIGHT / 2, playerPositionX - 5, playerPositionY + PLAYER_HEIGHT - 2, playerPositionX, playerPositionY + PLAYER_HEIGHT, WHITE);
  display.fillTriangle(playerPositionX + PLAYER_WIDTH, playerPositionY + PLAYER_HEIGHT / 2, playerPositionX + PLAYER_WIDTH + 5, playerPositionY + PLAYER_HEIGHT - 2, playerPositionX + PLAYER_WIDTH, playerPositionY + PLAYER_HEIGHT, WHITE);
}

void drawInvaders() {
  for (int i = 0; i < MAX_INVADERS; i++) {
    if (invaderActive[i]) {
      // Korpus
      display.fillRect(invaderPositionX[i], invaderPositionY[i], INVADER_SIZE, INVADER_SIZE / 2, WHITE);
      // Anteny
      display.drawLine(invaderPositionX[i], invaderPositionY[i], invaderPositionX[i] - 2, invaderPositionY[i] - 3, WHITE);
      display.drawLine(invaderPositionX[i] + INVADER_SIZE, invaderPositionY[i], invaderPositionX[i] + INVADER_SIZE + 2, invaderPositionY[i] - 3, WHITE);
      // Nogi
      display.drawLine(invaderPositionX[i], invaderPositionY[i] + INVADER_SIZE / 2, invaderPositionX[i] - 2, invaderPositionY[i] + INVADER_SIZE / 2 + 3, WHITE);
      display.drawLine(invaderPositionX[i] + INVADER_SIZE, invaderPositionY[i] + INVADER_SIZE / 2, invaderPositionX[i] + INVADER_SIZE + 2, invaderPositionY[i] + INVADER_SIZE / 2 + 3, WHITE);
    }
  }
  if (bossActive) {
// Korpus główny statku - większy, centralny prostokąt
    display.fillRect(bossPositionX, bossPositionY, BOSS_WIDTH, BOSS_HEIGHT, WHITE);
    
    // "Skrzydła" statku - mniejsze prostokąty po bokach
    display.fillRect(bossPositionX - 4, bossPositionY + 4, 4, 8, WHITE);
    display.fillRect(bossPositionX + BOSS_WIDTH, bossPositionY + 4, 4, 8, WHITE);

    // "Silniki" - dodatkowe prostokąty na dole korpusu głównego
    display.fillRect(bossPositionX + 4, bossPositionY + BOSS_HEIGHT, 4, 2, WHITE);
    display.fillRect(bossPositionX + BOSS_WIDTH - 8, bossPositionY + BOSS_HEIGHT, 4, 2, WHITE);

    // "Kokpit" - okrągły element na górze korpusu
    display.fillCircle(bossPositionX + BOSS_WIDTH / 2, bossPositionY + 4, 2, BLACK);

    // "Działa" - pionowe linie na froncie statku
    display.drawFastVLine(bossPositionX + 6, bossPositionY, 4, BLACK);
    display.drawFastVLine(bossPositionX + BOSS_WIDTH - 6, bossPositionY, 4, BLACK);

    // "Anteny" - cienkie linie na górze statku
    display.drawFastVLine(bossPositionX + BOSS_WIDTH / 2 - 6, bossPositionY - 3, 3, WHITE);
    display.drawFastVLine(bossPositionX + BOSS_WIDTH / 2 + 6, bossPositionY - 3, 3, WHITE);

    // Dekoracyjne linie wzmacniające wygląd technologiczny
    display.drawFastHLine(bossPositionX + 2, bossPositionY + 4, BOSS_WIDTH - 4, BLACK);
    display.drawFastHLine(bossPositionX + 2, bossPositionY + BOSS_HEIGHT - 4, BOSS_WIDTH - 4, BLACK);
  }
}

void drawPlayerBullets() {
  for (int i = 0; i < MAX_PLAYER_BULLETS; i++) {
    if (playerBulletActive[i]) {
      display.fillRect(playerBulletPositionX[i], playerBulletPositionY[i], BULLET_SIZE, BULLET_SIZE, WHITE);
    }
  }
}

void drawInvaderBullets() {
  for (int i = 0; i < MAX_INVADER_BULLETS; i++) {
    if (invaderBulletActive[i]) {
      display.fillRect(invaderBulletPositionX[i], invaderBulletPositionY[i], BULLET_SIZE, BULLET_SIZE, WHITE);
    }
  }
}

void shootPlayerBullet() {
  for (int i = 0; i < MAX_PLAYER_BULLETS; i++) {
    if (!playerBulletActive[i]) {
      playerBulletActive[i] = true;
      playerBulletPositionX[i] = playerPositionX + (PLAYER_WIDTH - BULLET_SIZE) / 2;
      playerBulletPositionY[i] = playerPositionY - BULLET_SIZE;
      break; // Strzelamy tylko jednym pociskiem naraz
    }
  }
}

void shootInvaderBullet_1() {
  if (invaderActive[0] && millis()- invaderLastShot[0] >= 2000) {
    invaderBulletActive[0] = true;
    invaderBulletPositionX[0] = invaderPositionX[0] + INVADER_SIZE / 2 - BULLET_SIZE / 2;
    invaderBulletPositionY[0] = invaderPositionY[0] + INVADER_SIZE;
    invaderLastShot[0] = millis();
  }
}

void shootInvaderBullet_2() {
  if (invaderActive[1] && millis() - invaderLastShot[1] >= 1750) {
    invaderBulletActive[1] = true;
    invaderBulletPositionX[1] = invaderPositionX[1] + INVADER_SIZE / 2 - BULLET_SIZE / 2;
    invaderBulletPositionY[1] = invaderPositionY[1] + INVADER_SIZE;
    invaderLastShot[1] = millis();
  }
}

void shootInvaderBullet_3() {
  if (invaderActive[2] && millis() - invaderLastShot[2] >= 1500) {
    invaderBulletActive[2] = true;
    invaderBulletPositionX[2] = invaderPositionX[2] + INVADER_SIZE / 2 - BULLET_SIZE / 2;
    invaderBulletPositionY[2] = invaderPositionY[2] + INVADER_SIZE;
    invaderLastShot[2] = millis();
  }
}

void shootInvaders() {
  shootInvaderBullet_1();
  shootInvaderBullet_2();
  shootInvaderBullet_3();
}

void shootSingle() {
  for (int i = 0; i < MAX_BOSS_BULLETS; i++) {
    if (!bossBulletActive[i]) {
      bossBulletActive[i] = true;
      bossBulletPositionX[i] = bossPositionX + BOSS_WIDTH / 2; // Środek bossa
      bossBulletPositionY[i] = bossPositionY + BOSS_HEIGHT; // Dolna część bossa
      bossBulletDirectionX[i] = 0; // Prosto w dół
      bossBulletDirectionY[i] = BOSS_BULLET_SPEED;
      break;
    }
  }
}

void shootCrossSalvo() {
  // Wystrzeliwujemy 4 pociski w różnych kierunkach
  // Tymczasowo zakładamy, że jest wystarczająco dużo miejsca w tablicy pocisków bossa
  for (int i = 0; i < 4; i++) {
    if (!bossBulletActive[i]) {
      bossBulletActive[i] = true;
      bossBulletPositionX[i] = bossPositionX + BOSS_WIDTH / 2;
      bossBulletPositionY[i] = bossPositionY + BOSS_HEIGHT / 2;
      // Kierunki: dół, w lewo, w prawo, góra
      bossBulletDirectionX[i] = (i == 1) ? -BOSS_BULLET_SPEED : (i == 2) ? BOSS_BULLET_SPEED : 0;
      bossBulletDirectionY[i] = (i == 0) ? BOSS_BULLET_SPEED : (i == 3) ? -BOSS_BULLET_SPEED : 0;
    }
  }
}

void shootWave() {
  // Atak falowy z poziomym ruchem sinusoidalnym
  for (int i = 0; i < MAX_BOSS_BULLETS; i++) {
    if (!bossBulletActive[i]) {
      bossBulletActive[i] = true;
      bossBulletPositionX[i] = bossPositionX + BOSS_WIDTH / 2;
      bossBulletPositionY[i] = bossPositionY + BOSS_HEIGHT;
      bossBulletDirectionX[i] = BOSS_BULLET_WAVE_AMPLITUDE; // Amplituda fali
      bossBulletDirectionY[i] = BOSS_BULLET_SPEED; // Prędkość w dół
      bossBulletPositionX2[i] = bossPositionX + BOSS_WIDTH / 2;
      bossBulletPositionY2[i] = bossPositionY + BOSS_HEIGHT;
      bossBulletDirectionX2[i] = BOSS_BULLET_WAVE_AMPLITUDE; // Amplituda fali
      bossBulletDirectionY2[i] = BOSS_BULLET_SPEED; // Prędkość w dół
      break;
    }
  }
}

void drawBossBullets() {
  for (int i = 0; i < MAX_BOSS_BULLETS; i++) {
    if (bossBulletActive[i]) {
      display.fillRect(bossBulletPositionX[i], bossBulletPositionY[i], BULLET_SIZE, BULLET_SIZE, WHITE);
    }
  }
}

void updateBossBullets() {
  for (int i = 0; i < MAX_BOSS_BULLETS; i++) {
    if (bossBulletActive[i]) {
      bossBulletPositionX[i] += bossBulletDirectionX[i];
      bossBulletPositionY[i] += bossBulletDirectionY[i];
      bossBulletPositionX2[i] -= bossBulletDirectionX2[i];
      bossBulletPositionY2[i] += bossBulletDirectionY2[i];

      // Logika dla ataku falowego
      if (bossAttackPattern == 2) {
          
      // Dodaj logikę ruchu dla ataku falowego
      bossBulletPositionY[i] += bossBulletDirectionY[i]; // Pociski poruszają się w dół

      // Sprawdzamy, czy pociski wyleciały poza ekran
      if (bossBulletPositionY[i] > SCREEN_HEIGHT) {
        bossBulletActive[i] = false;
      }
         
      }

      // Usuwamy pociski, które wyszły poza ekran
      if (bossBulletPositionY[i] > SCREEN_HEIGHT || bossBulletPositionY[i] < 0 ||
          bossBulletPositionX[i] > SCREEN_WIDTH || bossBulletPositionX[i] < 0) {
        bossBulletActive[i] = false;
      }
    }
  }
}


void checkCollisions() {
  for (int i = 0; i < MAX_PLAYER_BULLETS; i++) {
    if (playerBulletActive[i]) {
      for (int j = 0; j < MAX_INVADERS; j++) {
        if (invaderActive[j]) {
          if (playerBulletPositionX[i] >= invaderPositionX[j] - 1 && playerBulletPositionX[i] <= invaderPositionX[j] + INVADER_SIZE + 1 &&
              playerBulletPositionY[i] <= invaderPositionY[j] + INVADER_SIZE + 1 && playerBulletPositionY[i] >= invaderPositionY[j] - 1) {
            invaderActive[j] = false;
            playerBulletActive[i] = false;
            score++;

            // Sprawdzamy, czy wszyscy przeciwnicy zostali zestrzeleni
            bool allInvadersDefeated = true;
            for (int k = 0; k < MAX_INVADERS; k++) {
              if (invaderActive[k]) {
                allInvadersDefeated = false;
                break;
              }
            }

            if (allInvadersDefeated) {
              currentPhase++;
             /*if (currentPhase > 3) {
                gameActive = false;
                displayGameOver(); // Wyświetlanie ekranu końcowego 
                return; // Zakończenie pętli
              } else { */
                resetInvaders(currentPhase);
              
            }
            break; // Przerywamy pętlę, gdy pocisk trafi w przeciwnika
          }
        }
      }
    }
  }

  for (int i = 0; i < MAX_INVADER_BULLETS; i++) {
    if (invaderBulletActive[i]) {
      if (invaderBulletPositionX[i] >= playerPositionX && invaderBulletPositionX[i] <= playerPositionX + PLAYER_WIDTH &&
          invaderBulletPositionY[i] >= playerPositionY && invaderBulletPositionY[i] <= playerPositionY + PLAYER_HEIGHT) {
        invaderBulletActive[i] = false; // Deaktywacja pocisku przeciwnika
        playerLives--;
        hitFlash = true; // Aktywacja błysku ekranu

        if (playerLives <= 0) {
          gameActive = false;
          displayGameOver();
          return;
        }
      } else {
        invaderBulletPositionY[i] += 2;
        if (invaderBulletPositionY[i] > SCREEN_HEIGHT) {
          invaderBulletActive[i] = false;
        }
      }
    }
  }
  if (bossActive) {
    for (int i = 0; i < MAX_PLAYER_BULLETS; i++) {
        if (playerBulletActive[i] && playerBulletPositionX[i] >= bossPositionX && playerBulletPositionX[i] <= bossPositionX + BOSS_WIDTH && playerBulletPositionY[i] >= bossPositionY && playerBulletPositionY[i] <= bossPositionY + BOSS_HEIGHT) {
            bossHits++;
            playerBulletActive[i] = false; // Deaktywuj pocisk gracza po trafieniu
            if (bossHits >= BOSS_HIT_THRESHOLD) {
                bossActive = false; // Deaktywuj bossa po osiągnięciu limitu trafień
                // Tutaj możesz dodać logikę kończącą grę lub przechodzącą do kolejnej fazy
            }
        }
    if (bossBulletActive[i]) {
      if (bossBulletPositionX[i] >= playerPositionX && bossBulletPositionX[i] <= playerPositionX + PLAYER_WIDTH &&
          bossBulletPositionY[i] >= playerPositionY && bossBulletPositionY[i] <= playerPositionY + PLAYER_HEIGHT) {
        // Gracz trafiony
        bossBulletActive[i] = false;
        playerLives--;
        hitFlash = true;
        if (playerLives <= 0) {
          gameActive = false;
          displayGameOver();
          return;
        }
      }
    }
  
    
    
    }
}
if (!bossActive && currentPhase == 4) {
    currentPhase = 1; // Reset fazy do 1
    resetInvaders(currentPhase);
}


}

void displayGameOver() {
  display.clearDisplay();
  display.fillScreen(WHITE); // Błysk ekranu
  display.display();
  delay(100);
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);
  display.setCursor(20, 20);
  display.println("GAME OVER");
  display.setTextSize(1);
  display.setCursor(20, 40);
  display.print("SCORE: ");
  display.println(score);
  display.display();
  delay(5000);
  gameOver = true;
}

void resetGame() {
  // Resetujemy wszystkie zmienne do stanu początkowego gry
  score = 0;
  playerLives = 3;
  currentPhase = 1;
  gameOver = false;
  gameActive = true;
  
  // Dodatkowo resetujemy stany przeciwników i bossa
  resetInvaders(currentPhase);
  // ... inne niezbędne resetowania
}

void loop() {

  display.clearDisplay();
  if (hitFlash) {
    display.fillScreen(WHITE); // Błysk ekranu jako reakcja na trafienie
    hitFlash = false;
  } else {
    drawPlayer();
    drawInvaders();
    drawPlayerBullets();
    drawInvaderBullets();
  }

  if (digitalRead(BUTTON_LEFT) == LOW && playerPositionX > 0) {
    playerPositionX--;
  }
  if (digitalRead(BUTTON_RIGHT) == LOW && playerPositionX < SCREEN_WIDTH - PLAYER_WIDTH) {
    playerPositionX++;
  }

  static bool fireButtonPressed = false;
  if (digitalRead(BUTTON_FIRE) == LOW) {
    if (!fireButtonPressed) {
      shootPlayerBullet();
      fireButtonPressed = true;
    }
  } else {
    fireButtonPressed = false;
  }

  for (int i = 0; i < MAX_PLAYER_BULLETS; i++) {
    if (playerBulletActive[i]) {
      playerBulletPositionY[i] -= 2;
      if (playerBulletPositionY[i] < 0) {
        playerBulletActive[i] = false;
      }
    }
  }

   if (gameActive) {
    shootInvaders();
  }

  if (bossActive) {
    if (millis() - lastBossAttackTime > 2000) { // Co 2 sekundy boss wykonuje atak
      bossAttackPattern = (bossAttackPattern + 1) % 3; // Cykliczne przełączanie między wzorami ataków
      switch (bossAttackPattern) {
        case 0:
          shootSingle();
          break;
        case 1:
          shootCrossSalvo();
          break;
        case 2:
          shootWave();
          break;
      }
      lastBossAttackTime = millis();
    }
  }
  
  drawBossBullets();
  updateBossBullets();
  checkCollisions();

  

  display.display();
  delay(5);
}
Youtube
Tagi
ARDUINO UNO SPACE INVADERS