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.
1. Płytka Arduino UNO
2. Shield SIC Game Console
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
// 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();
}