Projekt umożliwia grę w “Rybki” na mikrokontrolerze Arduino z wyświetlaczem OLED 128×64px. Gra wzoruje się na przeglądarkowej grze “Rybki!” stworzonej przez użytkownika XGenStudios i dostępnej na stonie ttps://www.funnygames.pl/.
1. Płytka Arduino UNO lub kompatybilna
2. Shield SIC Game Console
Kod używa trzech bibliotek:
Wire – komunikacja I²C z wyświetlaczem
Adafruit_GFX – rysowanie kształtów i tekstu
Adafruit_SH110X – sterownik konkretnego wyświetlacza SH1106G
Dwa przyciski (piny 4 i 5) sterują rybką w górę i w dół.
Reguły gry:
Użytkownik ma możliwość poruszania się postacią - rybką - w górę i w dół ekranu za pomocą przycisków znajdujących się po lewej stronie konsoli SIC. Po ekranie przemieszczają się wypływające z prawej lub lewej strony ekranu ryby o różnych wielkościach. Aby wygrać poziom, użytkownik powinien manewrować swoją postacią tak, aby unikać ryb większych od siebie, a pożerać rybki mniejsze. Dzięki temu jego ryba rośnie i jest w stanie zjeść coraz to większe ryby.
Najważniejsze zasady działania kodu:
- Wykrywanie, czy użytkownik używa przycisków:
W każdej pętli sprawdzany jest input_pullup przycisków.
- Generowanie różnej wielkości ryb zaczynających swoją trasę z prawej lub lewej strony ekranu.
- Mechanizm kolizji oraz sprawdzenie, czy ryba, z którą odbyła się kolizja była większa czy mniejsza
Najważniejsze funkcje w kodzie:
- drawFish() - funkcja rysująca wszystkie ryby
W zależności od poziomu przypisanego rybie (od 1 do 4), rysowana jest odpowiedniej wielkości ryba.
Ogon ryby rysowany jest odpowiednio w zależności od kierunku, w którym się ona przemieszcza (goRight).
- updateBubbles() - funkcja rysująca bąbelki
W każdej klatce bąbelki przesuwają się o 1 piksel w górę. Gdy wyjdą poza ekran, odradzają się losowo na dole.
- spawnObj() - funkcja odradzająca przeciwników
Funkcja losuje:
- typ (rozmiar) – od 0 do playerLvl+1, więc gracz może spotkać ryby podobne do siebie
- stronę ekranu – lewa lub prawa, z losowym offsetem
- prędkość – rośnie z poziomem gry (1 + gameLevel/2)
- checkHit() - funkcja wykrywająca kolizję
Funkcja sprawdza nakładanie prostokątów gracza i NPC.
Sprawdzana jest wielkość rybki gracza w porównaniu z rybką przeciwnikiem, z którym się ona zderzyła:
- type < playerLvl → gracz zjada rybę → zwraca +1
- type > playerLvl → gracz ginie → zwraca -1
- równy poziom → brak reakcji → zwraca 0
Progresja i warunki końcowe
- Poziom gracza:
Gracz zaczyna będąc małą rybką oznaczoną indeksem wielkości “1”. Gdy zje dwie ryby mniejsze od siebie, jego ryba rośnie o jeden poziom wielkości, aż do poziomu “4” - maksymalnego.
- Poziom gry:
gdy gracz osiągnie maksymalny rozmiar (jego rybie przypisany jest indeks rozmiaru “4”) i zje jeszcze 2 ryby – przechodzi do następnego poziomu gry.
W następnym poziomie rybka wraca do swojej początkowej wielkości, a ryby “przeciwnicy” przyspieszają - stąd od gracza wymagana jest większa czujność i krótszy czas reakcji, by przejść do następnego poziomu, czy też ukończyć grę.
- Warunki końcowe:
Wygrana: ukończenie poziomu 5 przy max rozmiarze → ekran BRAWO!
Przegrana: zderzenie z większą rybą → ekran KONIEC!
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#define I2C_ADDR 0x3c
#define SCR_W 128
#define SCR_H 64
#define FRAME_MS 40
#define N_OBJ 3
#define N_BUBBLES 4
#define MAX_LVL 5
#define MAX_GAME_LVL 5
#define BTN_UP 4
#define BTN_DN 5
#define EAT_PER_GROW 2
static const uint8_t BW[] PROGMEM = { 6, 9, 13, 16, 20 };
static const uint8_t BH[] PROGMEM = { 4, 5, 7, 9, 11 };
static const uint8_t BR[] PROGMEM = { 2, 2, 3, 4, 5 };
static const uint8_t TL[] PROGMEM = { 3, 4, 5, 6, 8 };
static const uint8_t CW[] PROGMEM = { 9, 13, 18, 22, 28 };
static const uint8_t CH[] PROGMEM = { 4, 5, 7, 9, 11 };
#define PGM(arr, i) pgm_read_byte(&arr[i])
struct Obj {
int16_t x;
int8_t y;
int8_t spd;
uint8_t type;
bool active;
};
struct Bubble {
int16_t x;
int16_t y;
uint8_t r;
};
Adafruit_SH1106G display(SCR_W, SCR_H, &Wire, -1);
static Obj objs[N_OBJ];
static Bubble bubbles[N_BUBBLES];
static int8_t playerY = 26;
static uint8_t playerLvl = 1;
static uint8_t score = 0;
static uint8_t eatCount = 0;
static uint8_t gameLevel = 1;
static bool dead = false;
static bool won = false;
static uint32_t lastFrame = 0;
static uint16_t lfsr = 0xACE1u;
static uint8_t fastRand() {
lfsr ^= lfsr >> 7;
lfsr ^= lfsr << 9;
lfsr ^= lfsr >> 13;
return (uint8_t)lfsr;
}
static uint8_t rndRange(uint8_t lo, uint8_t hi) {
if (lo >= hi) return lo;
return lo + fastRand() % (hi - lo + 1);
}
void drawFish(int16_t x, int8_t y, uint8_t t, bool goRight) {
if (t >= 5) t = 4;
uint8_t ch = PGM(CH, t);
uint8_t cw = PGM(CW, t);
if (t == 0) {
display.fillCircle(x + cw / 2, y + ch / 2, ch / 2, SH110X_WHITE);
return;
}
uint8_t bw = PGM(BW, t);
uint8_t bh = PGM(BH, t);
uint8_t br = PGM(BR, t);
uint8_t tl = PGM(TL, t);
int8_t hy = y + bh / 2;
if (goRight) {
int16_t bodyX = x + tl;
display.fillTriangle(bodyX, hy, x, y, x, y + bh - 1, SH110X_WHITE);
display.fillRoundRect(bodyX, y, bw, bh, br, SH110X_WHITE);
display.drawPixel(bodyX + bw - 2 - (br / 4), y + 1 + (bh / 6), SH110X_BLACK);
} else {
int16_t tailX = x + bw;
display.fillTriangle(tailX, hy, tailX + tl, y, tailX + tl, y + bh - 1, SH110X_WHITE);
display.fillRoundRect(x, y, bw, bh, br, SH110X_WHITE);
display.drawPixel(x + 1 + (br / 4), y + 1 + (bh / 6), SH110X_BLACK);
}
}
void updateBubbles() {
for (uint8_t i = 0; i < N_BUBBLES; i++) {
bubbles[i].y -= 1;
if (bubbles[i].y < -10) {
bubbles[i].y = SCR_H + rndRange(5, 20);
bubbles[i].x = rndRange(5, SCR_W - 5);
bubbles[i].r = rndRange(1, 3);
}
display.drawCircle(bubbles[i].x, bubbles[i].y, bubbles[i].r, SH110X_WHITE);
}
}
void spawnObj(uint8_t i) {
uint8_t maxSpawn = (playerLvl < 4) ? playerLvl + 1 : 4;
uint8_t t = rndRange(0, maxSpawn);
objs[i].type = t;
objs[i].active = true;
objs[i].y = rndRange(2, SCR_H - PGM(CH, t) - 2);
int8_t sp = 1 + (gameLevel / 2);
if (fastRand() & 1) {
objs[i].x = -(int16_t)PGM(CW, t) - rndRange(10, 50);
objs[i].spd = sp;
} else {
objs[i].x = SCR_W + rndRange(10, 50);
objs[i].spd = -sp;
}
}
int8_t checkHit(const Obj& o) {
if (!o.active) return 0;
uint8_t pw = PGM(CW, playerLvl), ph = PGM(CH, playerLvl);
uint8_t ow = PGM(CW, o.type), oh = PGM(CH, o.type);
if (50 < o.x + ow && 50 + pw > o.x &&
playerY < o.y + oh && playerY + ph > o.y) {
if (o.type < playerLvl) return 1;
if (o.type > playerLvl) return -1;
}
return 0;
}
void resetGame() {
playerLvl = 1; playerY = 26; score = 0; eatCount = 0; gameLevel = 1;
dead = false; won = false;
for (uint8_t i = 0; i < N_OBJ; i++) spawnObj(i);
for (uint8_t i = 0; i < N_BUBBLES; i++) {
bubbles[i].x = rndRange(0, SCR_W);
bubbles[i].y = rndRange(0, SCR_H);
bubbles[i].r = rndRange(1, 3);
}
lastFrame = millis();
}
void waitForButton() {
delay(400);
while (digitalRead(BTN_UP) && digitalRead(BTN_DN)) { yield(); delay(10); }
delay(200);
}
void showStart() {
display.clearDisplay();
drawFish(45, 8, 4, true);
display.setTextSize(2);
display.setTextColor(SH110X_WHITE);
display.setCursor(35, 34); display.print(F("START"));
display.setTextSize(1);
display.setCursor(15, 54); display.print(F("[dowolny przycisk]"));
display.display();
waitForButton();
}
void showWin() {
display.clearDisplay();
display.setTextSize(2);
display.setCursor(30, 15); display.print(F("BRAWO!"));
drawFish(15, 20, 1, true);
drawFish(100, 20, 1, false);
display.setTextSize(1);
display.setCursor(10, 45); display.print(F("Wynik koncowy: ")); display.print(score);
display.setCursor(5, 56); display.print(F("[nacisnij aby zagrac]"));
display.display();
waitForButton();
resetGame();
}
void showDead() {
display.clearDisplay();
display.setTextSize(2);
display.setCursor(10, 15); display.print(F("KONIEC!"));
display.setTextSize(1);
display.setCursor(10, 40); display.print(F("Wynik: ")); display.print(score);
display.setCursor(10, 52); display.print(F("[dowolny przycisk]"));
display.display();
waitForButton();
resetGame();
}
void showLevelUp() {
display.clearDisplay();
display.setTextSize(2);
display.setCursor(20, 25); display.print(F("LEVEL ")); display.print(gameLevel);
display.display();
delay(1500);
}
void setup() {
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DN, INPUT_PULLUP);
delay(100);
Wire.begin();
Wire.setClock(400000);
Wire.setWireTimeout(5000, true);
display.begin(I2C_ADDR, true);
display.setRotation(2);
resetGame();
showStart();
}
void loop() {
yield();
uint32_t now = millis();
if (now - lastFrame < FRAME_MS) return;
lastFrame = now;
if (won) { showWin(); return; }
if (dead) { showDead(); return; }
if (!digitalRead(BTN_UP)) playerY -= 2;
if (!digitalRead(BTN_DN)) playerY += 2;
playerY = constrain(playerY, 1, SCR_H - PGM(CH, playerLvl) - 1);
display.clearDisplay();
updateBubbles();
for (uint8_t i = 0; i < N_OBJ; i++) {
objs[i].x += objs[i].spd;
if ((objs[i].spd > 0 && objs[i].x > SCR_W + 10) || (objs[i].spd < 0 && objs[i].x < -35)) {
spawnObj(i); continue;
}
int8_t hit = checkHit(objs[i]);
if (hit == 1) {
score++; eatCount++;
if (playerLvl < 4) {
if (eatCount >= EAT_PER_GROW) { eatCount = 0; playerLvl++; }
} else {
if (eatCount >= EAT_PER_GROW) {
eatCount = 0;
if (gameLevel >= MAX_GAME_LVL) { won = true; return; }
gameLevel++; playerLvl = 1;
showLevelUp();
for (uint8_t j = 0; j < N_OBJ; j++) spawnObj(j);
lastFrame = millis(); return;
}
}
spawnObj(i); continue;
}
if (hit == -1) { dead = true; return; }
if (objs[i].active) drawFish(objs[i].x, objs[i].y, objs[i].type, objs[i].spd > 0);
}
drawFish(50, playerY, playerLvl, true);
display.setTextSize(1);
display.setCursor(0, 0); display.print(score);
display.setCursor(80, 0); display.print(F("LVL ")); display.print(gameLevel);
display.display();
}