Rybki!

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

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/.

Niezbędne elementy

1. Płytka Arduino UNO lub kompatybilna

2. Shield SIC Game Console

Sprzęt

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ół.

 

Opis projektu

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:

  1. Wykrywanie, czy użytkownik używa przycisków:

W każdej pętli sprawdzany jest input_pullup przycisków.

  1. Generowanie różnej wielkości ryb zaczynających swoją trasę z prawej lub lewej strony ekranu.
  2. Mechanizm kolizji oraz sprawdzenie, czy ryba, z którą odbyła się kolizja była większa czy mniejsza

Najważniejsze funkcje w kodzie:

  1. 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).

  1. 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.

  1. 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)
  1. 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

  1. 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.

  1. 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ę.

  1. Warunki końcowe:

Wygrana: ukończenie poziomu 5 przy max rozmiarze → ekran BRAWO!

Przegrana: zderzenie z większą rybą → ekran KONIEC!

 

Zdjęcia
kod programu
#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();
}
Youtube
Tagi
#graarduino #gameplay #eattogrow