RatMAN

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

W ramach projektu stworzono autorską grę zręcznościową, która czerpie swoje inspiracje z szerokiego świata gier arcade i nosi doniosłą nazwę RatMAN. Gra została wgrana na moduł Arduino Uno oraz napisana w języku C++. 
W trakcie gry wcielisz się w zwinnego szczurka, który chce zjeść jak najwięcej sera unikając przy tym przeszkód. 

Niezbędne elementy

1. Płytka Arduino UNO

2. Shield SIC Game Console

Opis projektu

Ekran początkowy 

Gra zorientowana jest pionowo na ekranie 128x64 piksele. Ekran początkowy pozwala na wybranie jednego z trzech poziomów trudności: Easy, Hard lub Extreme. Sterowanie odbywa się przyciskami na konsoli. 

Reguły gry 

Zadaniem gracza jest jak najdłużej jeść ser. W trakcie gry czekają na niego przeszkody w postaci wystających widelców, za omijanie których użytkownik dostaje punkty. Aby ominąć widelec, gracz musi przeskoczyć na drugą stronę sera jednym z przycisków (prawo - lewo). Uderzenie w widelec kończy grę. Prędkość przesuwania się widelców zależy od wybranego poziomu trudności.  

Bonusy i kary 

Podczas gry, użytkownik może napotkać dodatkowe elementy. Są to: 

SER, który nagradza gracza dając mu +10 punktów

CZASZKA, której zebranie powoduje odjęcie -20 punktów

Ekran końcowy 

Po przegranej użytkownikowi wyświetli się ekran końcowy z podliczonymi zdobytymi punktami. 

Teraz czas na nową rozgrywkę! 

Obsługa wyświetlacza OLED 

Do obsługi wyświetlacza wymagane są następujące biblioteki: 

Adafruit_GFX.h - uniwersalna biblioteka graficzna napisana przez firmę Adafruit

Adafruit_SH110X.h - sterownik, który tłumaczy na ciąg specyficznych zer i jedynek obraz stworzony przez bibliotekę GFX 

Wire.h - biblioteka, przez którą Arduino wysyła dane do ekranu OLED

Zdjęcia
Bonusowy Ser
Ujemna Czaszka
Looser
kod programu
// Wymagane biblioteki 
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

#define i2c_Address 0x3c 
#define SCREEN_WIDTH 128 
#define SCREEN_HEIGHT 64 
#define OLED_RESET -1   

Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Przyciski 
#define PIN_LEFT_BTN 7
#define PIN_RESTART_BTN 8 
#define PIN_RIGHT_BTN 9 
#define PIN_DOWN_BTN 10   

// --- GRAFIKA SZCZURKA (lewo) ---
const unsigned char epd_bitmap_pixil_frame_0 [] PROGMEM = {
  0xdf, 0xff, 0xf0, 0xef, 0xff, 0xf0, 0xdf, 0xfd, 0xf0, 0xbc, 0x0c, 0xf0, 0xb1, 0xe4, 0xf0, 
  0xa7, 0xf3, 0x70, 0xaf, 0xfe, 0xb0, 0xcf, 0xf3, 0xd0, 0xcf, 0x84, 0x20, 0xe6, 0x37, 0xf0, 
  0xf1, 0x7b, 0xf0, 0xfd, 0xbf, 0xf0
};

// --- GRAFIKA SZCZURKA (prawo) ---
const unsigned char epd_bitmap_pixil_frame_0_mirrored [] PROGMEM = {
  0xff, 0xff, 0xb0, 0xff, 0xff, 0x70, 0xfb, 0xff, 0xb0, 0xf3, 0x03, 0xd0, 0xf2, 0x78, 0xd0, 
  0xec, 0xfe, 0x50, 0xd7, 0xff, 0x50, 0xbc, 0xff, 0x30, 0x42, 0x1f, 0x30, 0xfe, 0xc6, 0x70, 
  0xfd, 0xe8, 0xf0, 0xff, 0xdb, 0xf0
};

// Wymiary szczurka
const int szczurekW = 20; // szerokość 
const int szczurekH = 12;  // wysokość 

// --- GRAFIKA BONUSOWEGO SERA ---
const unsigned char epd_bitmap_bonus_cheese [] PROGMEM = {
  0x00, 0x30, 0x00, 0x64, 0x03, 0x0c, 0x0c, 0x66, 0x30, 0x92, 0xc0, 0x62, 0xff, 0xfe, 0x81, 0x82, 
  0xc2, 0x42, 0x21, 0x82, 0x20, 0x32, 0xc0, 0x4a, 0x88, 0x32, 0x94, 0x02, 0xff, 0xfe
};

// --- GRAFIKA CZASZKI ---
const unsigned char epd_bitmap_skull [] PROGMEM = {
  0x07, 0xc0, 0x38, 0x38, 0x40, 0x04, 0x94, 0x52, 0x88, 0x22, 0x94, 0x52, 0x80, 0x02, 0x80, 0x82, 
  0x42, 0x84, 0x30, 0x18, 0x10, 0x10, 0x08, 0x20, 0x0a, 0xa0, 0x0a, 0xa0, 0x05, 0x40
};

// Ustawienia gry
const int SEG_HEIGHT = 24;      // wysokość segmentu sera   
const int NUM_SEGMENTS = 6;     // numer segmentów wyświetlanych na ekranie
const int cheeseWidth = 15;     // szerokość sera 
const int cheeseX = (64 - cheeseWidth) / 2;     // wyśrodkowanie sera 

int segY[NUM_SEGMENTS];         
int forkType[NUM_SEGMENTS];  
int holeOffsetX[NUM_SEGMENTS];
int holeOffsetY[NUM_SEGMENTS];

// --- LOGIKA CZASU, PRĘDKOŚCI I PUNKTÓW ---
unsigned long gameStartTime = 0;
unsigned long lastMoveTime = 0;
unsigned long lastSpeedUpdateTime = 0;
int score = 0;

// Prędkość 
float currentMoveInterval = 5.0; 
const float minInterval = 1.0;    
int dropSpeed = 1; // Domyślnie Easy

// Pozycjonowanie szczurka i zjadanie sera 
bool isRatOnLeft = true;                        // Start gry z lewej strony 
const int ratY = 100;                           // Połozenie szczurka na ekranie 
const int eatingLine = ratY + 6;                
const int disappearLine = ratY + szczurekH + 3; // Znikanie sera 3 piksele nad szczurkiem 

bool gameOver = false;
bool showStartScreen = true; 
int menuSelection = 1; // 1 = Easy, 2 = Hard, 3 = Extreme

// --- ZMIENNE DO KONTROLOWANEJ LOSOWOŚCI ---
int lastForkType = -1;
int consecutiveForks = 0;
int forksSinceLastBonus = 0;
int nextBonusTarget = 15; 
int scoreAtLastSkull = 0;
int nextSkullTarget = 20; 

void setup() {
  display.begin(i2c_Address, true);
  display.setRotation(3); 
  
  pinMode(PIN_LEFT_BTN, INPUT_PULLUP);
  pinMode(PIN_RESTART_BTN, INPUT_PULLUP);
  pinMode(PIN_RIGHT_BTN, INPUT_PULLUP);
  pinMode(PIN_DOWN_BTN, INPUT_PULLUP); 
  
  randomSeed(analogRead(0));
  resetGame();
}

// Funkcja losowości pojawiania się widelcy i bonusów 
void randomizeSegment(int index, bool isInitial = false) {
  if (isInitial) {
    forkType[index] = 2; // Brak widelca na start
  } else {  // Ujemna czaszka i bonusowy ser 
    forksSinceLastBonus++;
    if (score >= scoreAtLastSkull + nextSkullTarget) {
      int side = random(0, 2); 
      forkType[index] = (side == 0) ? 5 : 6;   // Czaszka lewo 5, prawo 6 
      scoreAtLastSkull = score;                // Licznik punktacji 
      nextSkullTarget = random(20, 31);        // Losowość pojawiania się czaszki 
    } 
    else if (forksSinceLastBonus >= nextBonusTarget) {
      int bonusSide = random(0, 2); 
      forkType[index] = (bonusSide == 0) ? 3 : 4;   // Ser lewo 3, prawo 4  
      forksSinceLastBonus = 0;                 // Liczniki widelcy
      nextBonusTarget = random(10, 18);        // Losowość pojawiania się bonusowego sera
    } 
    else {
    int nextFork = random(0, 2); // Losowanie połozenia widelca 0 (lewo) lub 1 (prawo)
    
    // Sprawdzamy, czy wylosowaliśmy tę samą stronę co poprzednio
      if (nextFork == lastForkType) {
        consecutiveForks++;
      // Jeśli to już 4 raz z rzędu na tej samej stronie, wymuszamy zmianę
        if (consecutiveForks >= 4) {
          nextFork = 1 - nextFork; // 1-0=1, 1-1=0 (zmienia stronę)
          consecutiveForks = 1;    // Resetujemy licznik dla nowej strony
        }
      } else {
      // Jeśli strona jest inna, resetujemy licznik
      consecutiveForks = 1;
    }
    
    lastForkType = nextFork;
    forkType[index] = nextFork;
    }
  }
  
  holeOffsetX[index] = random(2, cheeseWidth - 4);
  holeOffsetY[index] = random(2, SEG_HEIGHT - 4); 
}

// Restartowanie gry 
void resetGame() {
  // Resetujemy liczniki powtarzalności przed każdą grą
  lastForkType = -1;
  consecutiveForks = 0;
  forksSinceLastBonus = 0;
  nextBonusTarget = random(10, 18);
  scoreAtLastSkull = 0;
  nextSkullTarget = random(20, 31);

  for (int i = 0; i < NUM_SEGMENTS; i++) {
    segY[i] = (i * SEG_HEIGHT) - SEG_HEIGHT; 
    randomizeSegment(i, true);
  }
  gameOver = false;
  isRatOnLeft = true;
  score = 0; 
  
  // Różne prędkości startowe dla poziomów trudności
  if (dropSpeed == 1) currentMoveInterval = 15.0;        // Easy 
  else if (dropSpeed == 2) currentMoveInterval = 10.0;   // Hard 
  else currentMoveInterval = 5.0;                        // Extreme 
  
  gameStartTime = millis();
  lastMoveTime = millis();
  lastSpeedUpdateTime = millis();
}

// Funkcja rysowania widelca 
void drawFork(int x, int y, bool isLeft) {
  int direction = isLeft ? -1 : 1;
  display.drawLine(x, y, x + (direction * 10), y, SH110X_WHITE);
  display.drawLine(x + (direction * 10), y, x + (direction * 14), y - 2, SH110X_WHITE);
  display.drawLine(x + (direction * 10), y, x + (direction * 14), y + 2, SH110X_WHITE);
}

void loop() {
  // --- EKRAN STARTOWY ---
  if (showStartScreen) {
    display.clearDisplay();
    // logo 
    display.drawBitmap(22, 10, epd_bitmap_pixil_frame_0, szczurekW, szczurekH, SH110X_BLACK, SH110X_WHITE);
    display.setTextSize(1);
    display.setTextColor(SH110X_WHITE);
    // nazwa 
    display.setCursor(14, 25); 
    display.print("RATMAN");
    // menu wyboru - poziomy trudności
    display.setCursor(5, 45);
    if (menuSelection == 1) display.print("> EASY"); else display.print("  EASY");
    
    display.setCursor(5, 57);
    if (menuSelection == 2) display.print("> HARD"); else display.print("  HARD");
    
    display.setCursor(5, 69);
    if (menuSelection == 3) display.print("> EXTREME"); else display.print("  EXTREME");
    
    display.setCursor(18, 90);
    display.print("Press");
    display.setCursor(18, 100);
    display.print("RIGHT");
    display.setCursor(10, 110);
    display.print("to play");
    
    display.display(); 
    // obsługa przycisków 
    if (digitalRead(PIN_RESTART_BTN) == LOW) { 
      menuSelection--;
      if (menuSelection < 1) menuSelection = 3;
      delay(150); 
    }
    if (digitalRead(PIN_DOWN_BTN) == LOW) { 
      menuSelection++;
      if (menuSelection > 3) menuSelection = 1;
      delay(150); 
    }

    if (digitalRead(PIN_RIGHT_BTN) == LOW) {
      if (menuSelection == 1) dropSpeed = 1;
      else if (menuSelection == 2) dropSpeed = 2;
      else dropSpeed = 3; 
      
      delay(200); 
      showStartScreen = false;
      resetGame(); 
    }
    return; 
  }

  // --- LOGIKA POWROTU DO MENU PO PRZEGRANEJ ---
  if (gameOver && digitalRead(PIN_RESTART_BTN) == LOW) {
    delay(200); 
    showStartScreen = true; 
    return; 
  }

  if (!gameOver && digitalRead(PIN_RESTART_BTN) == LOW) {
      delay(200);
      showStartScreen = true;
      return;
  }

  if (!gameOver) {
    if (digitalRead(PIN_LEFT_BTN) == LOW) isRatOnLeft = true;
    if (digitalRead(PIN_RIGHT_BTN) == LOW) isRatOnLeft = false;

    unsigned long currentTime = millis();
    unsigned long playTime = currentTime - gameStartTime;

    if (playTime > 2000) {  
      if (currentTime - lastSpeedUpdateTime >= 10) { 
        lastSpeedUpdateTime = currentTime;
        if (currentMoveInterval > minInterval) {
          currentMoveInterval -= 2; 
        }
      }
    }

    if (currentTime - lastMoveTime >= (unsigned long)currentMoveInterval) {
      lastMoveTime = currentTime;

      for (int i = 0; i < NUM_SEGMENTS; i++) {
        segY[i] += dropSpeed; 
        
        if (segY[i] >= 150) {
          segY[i] -= (NUM_SEGMENTS * SEG_HEIGHT); 
          randomizeSegment(i, false);
        }

        if (forkType[i] != 2) {                // jeśli jest widelec, czaszka lub ser 
          int forkY = segY[i] + (SEG_HEIGHT / 2); 
          
          int targetLine = ratY + szczurekH + 1;
          if (forkY >= targetLine && forkY < targetLine + dropSpeed) {
             // licznik ominiętych widelcy (punktacja)
             if (forkType[i] == 0 || forkType[i] == 1) score++;     
          }

          // Warunki przegrania gry i punktacji
          if (forkY >= ratY && forkY <= ratY + szczurekH) {
            bool ratLeft = isRatOnLeft;
            // Zderzenie z widelcem 
            if (forkType[i] == 0 && ratLeft) gameOver = true;
            if (forkType[i] == 1 && !ratLeft) gameOver = true;
            // Punktacja bonusowego sera (+10)
            if (forkType[i] == 3 && ratLeft) { score += 10; forkType[i] = 2; }
            if (forkType[i] == 4 && !ratLeft) { score += 10; forkType[i] = 2; }
            // Punktacja czaszki (-20)
            if (forkType[i] == 5 && ratLeft) {
              score -= 20; forkType[i] = 2;  
              // Ujemna punktacja po złapaniu czaszki - GAME OVER
              if (score < 0) gameOver = true;    
            }
            if (forkType[i] == 6 && !ratLeft) {
              score -= 20; forkType[i] = 2; 
              // Ujemna punktacja po złapaniu czaszki - GAME OVER
              if (score < 0) gameOver = true; 
            }
          }
        }
      }
    }
  }

  // --- RYSOWANIE GRY ---
  display.clearDisplay();

  for (int i = 0; i < NUM_SEGMENTS; i++) {
    int y = segY[i];
    int visibleHeight = SEG_HEIGHT;
    if (y + SEG_HEIGHT > eatingLine) visibleHeight = eatingLine - y; 
    
    // Ser na środku ekranu 
    if (visibleHeight > 0 && y < 128) {
      display.fillRect(cheeseX, y, cheeseWidth, visibleHeight, SH110X_WHITE);
      int holeY = y + holeOffsetY[i];
      if (holeY < eatingLine) display.fillCircle(cheeseX + holeOffsetX[i], holeY, 2, SH110X_BLACK);
    }
    // Widelce i bonusy   
    if (forkType[i] != 2 && y < 128 && y > -SEG_HEIGHT) {
      int forkY = y + (SEG_HEIGHT / 2); 
      if (forkY <= disappearLine) {
        if (forkType[i] == 0) drawFork(cheeseX, forkY, true);
        else if (forkType[i] == 1) drawFork(cheeseX + cheeseWidth, forkY, false); 
        else if (forkType[i] == 3) display.drawBitmap(cheeseX - 20, forkY - 7, epd_bitmap_bonus_cheese, 15, 15, SH110X_WHITE); 
        else if (forkType[i] == 4) display.drawBitmap(cheeseX + cheeseWidth + 5, forkY - 7, epd_bitmap_bonus_cheese, 15, 15, SH110X_WHITE); 
        else if (forkType[i] == 5) display.drawBitmap(cheeseX - 20, forkY - 7, epd_bitmap_skull, 15, 15, SH110X_WHITE); 
        else if (forkType[i] == 6) display.drawBitmap(cheeseX + cheeseWidth + 5, forkY - 7, epd_bitmap_skull, 15, 15, SH110X_WHITE); 
      }
    }
  }

  // Szczurek
  int szczurekX = isRatOnLeft ? (cheeseX - szczurekW - 3) : (cheeseX + cheeseWidth + 3);
  const unsigned char* currentBitmap = isRatOnLeft ? epd_bitmap_pixil_frame_0 : epd_bitmap_pixil_frame_0_mirrored;
  display.drawBitmap(szczurekX, ratY, currentBitmap, szczurekW, szczurekH, SH110X_BLACK, SH110X_WHITE);

  // Punktacja
  if (!gameOver) {
    display.setTextSize(1);
    display.setTextColor(SH110X_WHITE);
    display.setCursor(15, 120); 
    display.print("Pkt: ");
    display.print(score);
  }

  // Ekran GAME OVER
  if (gameOver) {
    display.fillRect(0, 42, 64, 40, SH110X_BLACK);
    
    display.setCursor(10, 45); 
    display.print("LOOSER");
    
    display.setCursor(5, 57); 
    display.print("SCORE: ");
    display.print(score);

    display.setCursor(5, 69); 
    display.print("NEW GAME"); 
    
    int arrX = 52; int arrY = 69;
    display.drawLine(arrX + 2, arrY, arrX, arrY + 2, SH110X_WHITE);
    display.drawLine(arrX + 2, arrY, arrX + 4, arrY + 2, SH110X_WHITE);
    display.drawLine(arrX + 2, arrY, arrX + 2, arrY + 4, SH110X_WHITE);
  }

  display.display();
}
Pliki_projektu
Schemat
Youtube
Tagi
arduino sic gra konsola oled