Who's that Pokemon?

Introducere

Sigur toată lumea s-a jucat la un moment dat Spanzurătoarea și sunt destul de sigur că toată lumea a auzit la un moment dat de Pokemon. Ce am încercat să aduc aici e practic un mix distractiv între cele două, bazat pe quiz-ul cu același nume care mai difuza în timpul episoadelor din anime, cu diferența evidentă că nu ai forma Pokemon-ului ca indiciu, ci ai tipul lui și numărul de litere.

Descriere generală

Pe ecranul LCD este afișată inițial interfața introductivă a jocului, care îți dă prompt să apeși butonul de Select din mijloc pentru a începe jocul propriu-zis. Odată început, va fi ales un Pokemon aleator și vor fi afișate pe LCD următoarele informații în ordinea asta:

  • Tipul Pokemon-ului(Fire, Normal/Flying etc.) și nivelul curent
  • Spațiile de completat reprezentate prin underscore-uri inițial
  • Litera curentă
  • HP bar-ul care desemnează câte vieți mai ai

Ca să poți să ciclezi printre litere te vei folosi de butoanele de Left și Right situate in părțile respective pe breadboard. Odată aleasă litera dorită, va trebui să apeși Select pentru a confirma alegerea. Dacă ai ales bine, se vor completa toate spațiile corespondente literei și speaker-ul va reda un sunet 'afirmativ'. Altfel, speaker-ul va reda un sunet 'negativ' și ți se va scădea 1 punct din HP. Odată ce ai selectat o literă, indiferent de rezultat, vei sări peste ea la ciclări viitoare prin alfabet.

Jocul poate progresa în două moduri:

  • dacă ai ghicit tot numele
    • se va afișa un mesaj corespunzător
    • vei trece la nivelul următor
    • se va alege un nou Pokemon
    • HP bar-ul se va reseta
  • dacă HP-ul a ajuns la 0
    • se va reseta HP bar-ul și nivelul

De asemenea, am utilizat speaker-ul și pentru a reda melodii care să te acompanieze în timpul jocului, toate provenite din jocurile Pokemon. Vei avea de a face cu 3 pe parcursul lui:

  • Prima o vei auzi atunci când rulezi programul, la ecranul introductiv.
  • A doua o vei auzi pe parcursul jocului.
  • A treia o va înlocui pe cea precedentă atunci când mai rămâi cu o viață

Hardware Design

Listă de piese:

  • Arduino Uno R3
  • Ecran LCD 20×4
  • Adaptor IIC pentru LCD 20×4
  • Difuzor dreptunghiular 1511 cu fir
  • Fire female-female și male-male
  • Breadboard
  • 3 * Modul Buton 6×6 mm
  • 3 * Rezistor 10kohm
  • 6 * Baterii AA + carcasă
  • Placă de lemn pentru montat componente
  • 4 piese dintr-un cub Lego pe post de picioruțe pentru placă

Proiectul după asamblare:

De precizat este faptul că adiția bateriilor a fost o idee de ultim moment pentru portabilitatea proiectului, motiv pentru care nu se regăsește pe schematici.

Software Design

Am scris codul în Arduino IDE și am utilizat biblioteca:

  • LiquidCrystal_I2C.h: o variantă modificată a bibliotecii LiquidCrystal.h care facilitează comunicarea între placuța Arduino, adaptorul I2C și ecranul LCD.

Variabile globale

1. Constante pentru note în functie de frecvență și melodii
#define B0  31
#define C1  33
#define CS1 35
#define D1  37
#define DS1 39
#define E1  41
#define F1  44
#define FS1 46
#define G1  49
#define GS1 52
#define A1  55
.
.
.
#define Rest 0
 
// Într-un vector de melodie, fiecare element de tip 'notă muzicală' 
// este urmat de durata lui, cu precizarea că nota va fi redată 
// pentru 1 supra valoarea din vector. Raportat la un music sheet,
// de exemplu, 8 desemnează o optime,
// 4 o pătrime, 2 o doime ș.a.m.d.
 
// Duratele negative de fapt arată ca nota va fi prelungită cu 
// jumătate din durata ei. Aș fi putut să scriu altfel
// codul, dar e mai ușor atunci când vreau sa urmăresc 
// un portativ în paralel.
 
int melody[] = {
 
  C5,8, G4,8, C5,8, G5,8, REST,8, F5,8, REST,8, E5,8,
  D5,8, B4,-4, REST,2,
  B4,8, G4,8, B4,8, F5,8, REST,8, E5,8, REST,8, D5,8,
  C5,8, E5,-4, REST,2,
  C5,8, G4,8, C5,8, G5,8, REST,8, F5,8, REST,8, E5,8,
  D5,8, B4,-4, REST,2,
  B4,8, G4,8, B4,8, F5,8, REST,8, E5,8, REST,8, D5,8,
  C5,2, C5,8, B4,8, C5,8, D5,8,
  E5,2, G5,-4,
  D5,16, E5,16, F5,8, G5,8, F5,8, E5,8, D5,2,
  B4,2, D5,-4, C5,16, D5,16,
  E5,8, F5,8, E5,8, D5,8, C5,4, G5,8, E5,16, D5,16,
  E5,2, G5,-4, D5,16, E5,16,
  F5,8, E5,8, F5,8, G5,8, A5,4, D5,16, E5,16, F5,8,
  G5,4, F5,8, E5,8, F5,2,
  E5,8, F5,8, E5,8, D5,8, C5,2
};
 
int low_hp[] = {
  ...
};
 
int driftveil[] = {
  ...
};
2. Constante și variabile pentru melodii
const int buzzer = 11; // Pin-ul pentru buzzer
 
int tempo = 120;
int notes = sizeof(melody) / sizeof(melody[0]) / 2;
 
int wholenote = (60000 * 4) / tempo;
 
int divider = 0, noteDuration = 0;
unsigned long lastNoteTime = 0;
int noteIndex = 0;
 
bool feedbackPlaying = false;      // pentru sincronizarea sunetelor de feedback
unsigned long feedbackEndTime = 0; // la selectarea unei litere
 
// Seturi separate de variabile (de exemplu, tempo_low, noteIndex_low) 
// gestionează melodii diferite (low_hp, driftveil).
 
int tempo_low = 175;
int notes_low = sizeof(low_hp) / sizeof(low_hp[0]) / 2;
 
int wholenote_low = (60000 * 4) / tempo_low;
 
int divider_low = 0, noteDuration_low = 0;
unsigned long lastNoteTime_low = 0;
int noteIndex_low = 0;
 
int tempo_drift = 130;
int notes_drift = sizeof(driftveil) / sizeof(driftveil[0]) / 2;
 
int wholenote_drift = (60000 * 4) / tempo_drift;
 
int divider_drift = 0, noteDuration_drift = 0;
unsigned long lastNoteTime_drift = 0;
int noteIndex_drift = 0;
 
#define CORRECT_NOTE C6       // sunet pentru feedback pozitiv
#define INCORRECT_NOTE G4     // sunet pentru feedback negativ
#define FEEDBACK_DURATION 500 // durata sunetelor de feedback
3. Definiții pentru Pokemon
// listează tipurile posibile de Pokemon
typedef enum {
    Grass,
    Poison,
    Fire,
    ...
    None
} PokemonType;
 
// Un Pokemon este definit de numele lui și de posibilele 2 tipuri ale lui
typedef struct {
    char name[20];
    PokemonType type1;
    PokemonType type2;
} Pokemon;
 
// Stocăm 150 de elemente de tip Pokemon în memoria programului 
// cu modificatorul PROGMEM
const Pokemon pokemon_list[] PROGMEM = {
    {"BULBASAUR", Grass, Poison},
    {"IVYSAUR", Grass, Poison},
    {"VENUSAUR", Grass, Poison},
    ...
    {"MEW", Psychic, None}
};
4. Configurarea LCD-ului și Butoanelor
LiquidCrystal_I2C lcd(0x27, 20, 4);
 
// Pin-urile pentru butoane
const int SelectBtnPin = 2;
const int LeftBtnPin = 3;
const int RightBtnPin = 4;
 
// Stările pentru butoane
int lastSelectState = LOW;
int lastLeftState = LOW;
int lastRightState = LOW;
5. Variabile de stare a jocului
int HP = 6;
int Level = 1;
char displayedName[20];
char hiddenName[20];
bool letterSelected[26]; // gestionează literele deja selectate
                         // de-a lungul unui nivel
bool pokemonUsed[150]; // gestionează pokemonii deja ghiciți
                       // de-a lungul jocului
char selectedLetter = 'A'; // litera curentă
bool gameInProgress = false; // variabilă care face tranziția de la
                             // ecranul introductiv la joc
 
// Caractere speciale pentru desenarea HP bar-ului.
// Fiecare căsuță de pe LCD este o matrice de 8x5 pixeli
// care pot fi aprinși individual.
byte leftbar[] = { 0xF, 0x10, 0x17, 0x17, 0x17, 0x17, 0x10, 0xF };
byte rightbar[] = { 0x1E, 0x01, 0x1D, 0x1D, 0x1D, 0x1D, 0x01, 0x1E };
byte middlebar[] = { 0x1F, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x1F};
byte emptybar[] = {0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F};
byte emptyrightbar[] = {0x1E, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x1E};
byte emptyleftbar[] = {0xF, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xF};

Funcții utilizate

  • setup():

Configurează LCD-ul și inițializează pinii butoanelor, afișează un mesaj introductiv pe LCD și setează generatorul de numere aleatorii.

void setup() {
    lcd.init();
    lcd.backlight();
    pinMode(SelectBtnPin, INPUT);
    pinMode(LeftBtnPin, INPUT);
    pinMode(RightBtnPin, INPUT);
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Who's that Pokemon?");
    lcd.setCursor(2, 2);
    lcd.print("Select to Start");
    randomSeed(millis());
}
  • loop():

Bucla principală a jocului gestionează intrările de la butoane pentru selectarea literelor și confirmarea ghicirilor, actualizează starea jocului în funcție de ghicirile corecte sau incorecte, redă melodia adecvată în funcție de starea jocului (melodia introductivă, melodia principală, melodia pentru HP scăzut, sunetele de feedback) și actualizează LCD-ul cu informațiile jocului.

void loop() {
  int selectState = digitalRead(SelectBtnPin);
  int leftState = digitalRead(LeftBtnPin);
  int rightState = digitalRead(RightBtnPin);
 
  if (selectState != lastSelectState) {
    if (selectState == HIGH) {
      if (!gameInProgress) { // Start game condition
        startGame();
      } else {
        letterSelected[selectedLetter - 'A'] = true;
        bool found = false;
        for (int i = 0; hiddenName[i] != '\0'; i++) {
          if (hiddenName[i] == selectedLetter) {
            displayedName[i] = selectedLetter;
            found = true;
          }
        }
        if (!found) {
          HP--;
          tone(buzzer, INCORRECT_NOTE, FEEDBACK_DURATION);
        } else {
          tone(buzzer, CORRECT_NOTE, FEEDBACK_DURATION);
        }
        feedbackPlaying = true;
        feedbackEndTime = millis() + FEEDBACK_DURATION + 500;
 
        lcd.setCursor(0, 1);
        lcd.print(displayedName);
        draw_healthbar();
 
        if (HP <= 0) {
          gameInProgress = false;
          lcd.setCursor(0, 2);
          lcd.print("You fainted!");
          Level = 1;
          memset(pokemonUsed, 0, sizeof(pokemonUsed));
          delay(2000);
          resetGame();
        } else if (strcmp(displayedName, hiddenName) == 0) {
          lcd.setCursor(0, 2);
          lcd.print("Level up!");
          Level++;
          delay(2000);
          resetGame();
        }
      }
    }
    lastSelectState = selectState;
  }
  if (leftState != lastLeftState) {
      if (leftState == HIGH && gameInProgress) {
          do {
              selectedLetter--;
              if (selectedLetter < 'A') selectedLetter = 'Z';
          } while (letterSelected[selectedLetter - 'A']);
          lcd.setCursor(0, 2);
          lcd.print(selectedLetter);
      }
      lastLeftState = leftState;
  }
  if (rightState != lastRightState) {
      if (rightState == HIGH && gameInProgress) {
          do {
              selectedLetter++;
              if (selectedLetter > 'Z') selectedLetter = 'A';
          } while (letterSelected[selectedLetter - 'A']);
          lcd.setCursor(0, 2);
          lcd.print(selectedLetter);
      }
      lastRightState = rightState;
  }
  if (!gameInProgress){
    playMelody(melody, noteIndex, notes, wholenote, lastNoteTime);
  } else if (!feedbackPlaying) {
      if (HP > 1) {
          playMelody(driftveil, noteIndex_drift, notes_drift, wholenote_drift, lastNoteTime_drift);
      } else {
          playMelody(low_hp, noteIndex_low, notes_low, wholenote_low, lastNoteTime_low);
      }
  } else {
      if (millis() >= feedbackEndTime) {
          feedbackPlaying = false;
      }
  }
}
  • startGame():

Inițializează o nouă rundă de joc, selectează un Pokémon aleator din listă și actualizează LCD-ul cu tipul acestuia, inițializează displayedName cu caractere de subliniere care reprezintă numele Pokémon-ului ascuns, afișează nivelul curent și desenează bara de viață.

void startGame() {
    lcd.clear();
    int randomIndex;
    do {
        randomIndex = random(0, n);
    } while (pokemonUsed[randomIndex]);
    pokemonUsed[randomIndex] = true;
    strcpy_P(hiddenName, pokemon_list[randomIndex].name);
    PokemonType type1 = (PokemonType)pgm_read_byte(&pokemon_list[randomIndex].type1);
    PokemonType type2 = (PokemonType)pgm_read_byte(&pokemon_list[randomIndex].type2);
 
    lcd.setCursor(0, 0);
    lcd.print(getTypeName(type1));
    if (type2 != None) {
        lcd.print("/");
        lcd.print(getTypeName(type2));
    }
 
    for (int i = 0; hiddenName[i] != '\0'; i++) {
        displayedName[i] = '_';
    }
    displayedName[strlen(hiddenName)] = '\0';
 
    lcd.setCursor(0, 1);
    lcd.print(displayedName);
    lcd.setCursor(0, 2);
    lcd.print(selectedLetter);
    if(Level < 10) {
      lcd.setCursor(16, 0);
    } else {
      lcd.setCursor(15, 0);
    }
    lcd.print("Lv.");
    lcd.print(Level);
    draw_healthbar();
    gameInProgress = true;
}
  • resetGame():

Resetează variabilele jocului pentru a începe un nou joc: HP-ul, lista de litere selectate și indicii melodiilor. Ea în plus mai setează generatorul de numere aleatorii și apelează startGame.

void resetGame() {
    HP = 6;
    memset(letterSelected, 0, sizeof(letterSelected));
    noteIndex_drift = 0;
    noteIndex_low = 0;
    randomSeed(millis() + analogRead(5));
    startGame();
}
  • draw_healthbar():

Folosește caractere personalizate pentru a desena o bara de viață pe LCD și actualizează afișajul în funcție de HP curent.

void draw_healthbar() {
  lcd.createChar(0, leftbar);
  lcd.createChar(1, rightbar);
  lcd.createChar(2, middlebar);
  lcd.createChar(3, emptybar);
  lcd.createChar(4, emptyrightbar);
  lcd.createChar(5, emptyleftbar);
 
  if (HP == 0){
    lcd.setCursor(0,3);
    lcd.write(5);
  } else {
    lcd.setCursor(0,3);
    lcd.write(0);
  }
  for(int i = 1; i <= HP*3; i++){
    lcd.setCursor(i,3);
    lcd.write(2);
  }
  for (int i = HP*3 + 1; i <= 18; i++){
    lcd.setCursor(i,3);
    lcd.write(3);
  }
  if(HP < 6){
    lcd.setCursor(19,3);
    lcd.write(4);
  } else {
    lcd.setCursor(19,3);
    lcd.write(1);
  }
}
  • playMelody():

Gestionează redarea unei melodii într-un loop, folosind noteIndex și noteDuration pentru a determina ce notă să redea și pentru cât timp. De asemenea, actualizează lastNoteTime pentru a gestiona sincronizarea dintre note.

void playMelody(int* melody, int& noteIndex, int notes, int wholenote, unsigned long& lastNoteTime) {
    if (noteIndex <= notes * 2 && millis() - lastNoteTime >= noteDuration) {
        int note = melody[noteIndex];
        int duration = melody[noteIndex + 1];
 
        if (duration > 0) {
            noteDuration = wholenote / duration;
        } else if (duration < 0) {
            noteDuration = (wholenote / abs(duration)) * 1.5;
        }
 
        if (note == REST) {
            noTone(buzzer);
        } else {
            tone(buzzer, note, noteDuration * 0.9);
        }
 
        lastNoteTime = millis();
        noteIndex += 2;
    } else if (noteIndex > notes * 2) {
        noTone(buzzer);
        noteIndex = 0;
    }
}

Rezultate Obţinute

Totul a mers mult mai bine decât mă așteptam inițial și chiar ma bucur din tot sufletul pentru rezultat

Concluzii

A fost un proiect cu totul mișto, nu am ce zice. E prima dată când realizez un joc video cu care pot să interacționez fizic per se și chiar sunt foarte entuziasmat. Hardware-ul a fost ușor de conceput, dar nu pe asta am vrut să pun accent prin acest proiect. Sincer, inițial chiar nu știam ce voi ajunge să prezint, dar aveam în cap toate componentele necesare pentru realizarea oricărui joc, indiferent de complexitatea lui. Am avut totuși nevoie de ceva documentație pentru asamblarea pieselor, pentru că hardware-ul nu este punctul meu forte. Software-ul a fost cea mai discractivă parte, mi-a plăcut foarte mult să imi proiectez caractere speciale și să îmi compun melodii de la 0(dar bine, inițial trebuia să mai fie și un cititor de card SD în plan, care nu mi-a ajuns din motive diverse, pentru a reda melodii și a încărca asset-uri pe placă pentru difuzarea pe LCD, totuși simt că așa am avut o libertate mai mare în a-mi defini parametrii jocului după propriul plac). Am mai avut de a face cu proiectarea de jocuri la cursul de EGC de semestrul trecut, dar aici a fost ceva nou, în sensul în care nu am avut niciun schelet ajutător(din nou, simt că libertatea asta mi-a priit bine).

Sincer, a fost o experiență super satisfying, mai ales că tot hardware-ul poate fi reutilizat foarte ușor pentru proiecte personale viitoare și cine știe, poate voi face un joc mai interactiv data viitoare. Cu Arduino chiar ai posibilități cvasi-nelimitate în ceea ce privește asta și chiar voi vrea să testez limitele lui pe cât de mult posibil, în viitor.

Până atunci, sper ca jocul meu va oferi o experiență drăguță, pentru mine Pokemon e una dintre cele mai nostalgice francize cu privire la copilăria mea, și mă bucur că voi putea oferi o fereastră către trecut la oricine va fi interesat la târg.

Download

Bibliografie/Resurse

Hardware:

Software:

pm/prj2024/azamfir/rares.nistor.txt · Last modified: 2024/05/26 22:20 by rares.nistor
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0