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.
1. Płytka Arduino UNO
2. moduł SIC Game Console v0.2 beta
3. Wyświetlacz IIC OLED SSD1306
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:
-
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.
-
Wynik: Gracz zdobywa punkty za każdego zestrzelonego kosmitę. Wynik wyświetla się na końcu rozgrywki.
-
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.
-
Strategia i zręczność: Sukces w grze wymaga zarówno strategicznego planowania, jak i zręczności w manewrowaniu i strzelaniu.
#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);
}