Stanca Adelin-Nicolae 331CA
Prezentarea pe scurt a proiectului:
Jocul debuteaza in momentul in care este apasat de catre pen ecranul display-ului LCD TFT. La inceput este generat un punct random pe display, dupa care se va genera playerul care va urmari miscarea penului (sau ultima sa miscare). Scopul este de a atinge punctul random, moment in care se incrementeaza dimensiunea playerului si se genereaza un alt punct. Jocul se incheie in momentul in care jucatorul se loveste de sine insusi sau in momentul in care penul il dirijeaza catre un perete, moment in care se va afisa pe LCD scorul final, precum si scorul maxim. Pentru a reseta jocul, la 10 secunde de afisare a scorului, jocul se reinitializeaza, iar jucatorul trebuie doar sa atinga o data ecranul pentru a reincepe jocul.
Am reusit in cele din urma sa lipesc cele 2 componente principale, fapt ce m-a ajutat ca la final sa nu mai am nevoie de majoritatea componentelor auxiliare, precum firele si placuta.
Toata functionalitatea este implementata in cadrul fisierului inceput.ino in cadrul caruia am incercat sa conserv o abordare cat se poate de modularizata, iar codul sa fie cat se poate de lizibil. Jocul debuteaza cu functia de setup care practic afiseaza la LCD numele proiectului pentru pagina de inceput, dupa care se initializeaza bara de Loading implementata tot in aceeasi functie, insa care va aparea separat pe ecran.
void setup() { tft.reset(); tft.begin(0x9341); tft.setRotation(1); tft.fillScreen(BLACK); tft.setCursor(55, 80); tft.setTextSize(3); tft.setTextColor(GREEN); tft.println("Snake Game"); tft.setCursor(85, 120); tft.setTextSize(1); tft.setTextColor(WHITE); tft.println("Proiect realizat de"); tft.setCursor(70, 150); tft.setTextSize(2); tft.setTextColor(BLUE); tft.println("Adelin Stanca"); delay(1000); tft.fillScreen(BLACK); tft.setCursor(95, 110); tft.setTextColor(GREEN); tft.println("Loading..."); tft.drawRect(60, 140, 200, 20, GREEN); for (int i = 0; i < 100; i++) { tft.fillRect(60, 140, i * 2, 20, GREEN); if (i == 50) { delay(150); } delay(50); } tft.fillScreen(color); }
Exista 4 stari principale pe care jocul meu le traverseaza, acestea sunt setupGame, pausedGame, inGame si gameLost. Fiecarei stari ii corespunde o functie principala la care se adauga altele auxiliare. Jocul debuteaza din starea gameSetup. In cadrul acelei stari, practic se genereaza o pozitie random pentru jucator, apoi o pozitie aleatoare pentru 'hrana' sarpelui, tinandu-se cont de pozitiile ocupate deja de sarpe. Dupa aceste lucruri, se trece automat in starea pausedGame, unde practic se asteapta o prima atingere cu penul a ecranului pentru a debuta jocul. Pentru folosirea penului am avut nevoie de biblioteca Touchscreen pe care am folosit-o in felul urmator:
void gamePaused() { TSPoint p = ts.getPoint(); pinMode(XM, OUTPUT); pinMode(YP, OUTPUT); if ((p.z > MINPRESSURE) && (p.z < MAXPRESSURE)) { p.x = map(p.x, TS_MINX, TS_MAXX, 0, tft.width()); p.y = 240 - map(p.y, TS_MINY, TS_MAXY, 0, tft.height()); state = inGame; } draw(); delay(300); }
Odata ce jocul a pornit, trec in starea inGame in care se petrec urmatoarele 3 lucruri: verificarea coliziunilor, preluarea inputului si updatarea valorilor si a ecranului. Mai intai, pentru verificarea coliziunilor am verificat daca jucatorul atinge 'hrana', caz in care se genereaza o noua 'hrana', se incrementeaza dimensiunea jucatorului si i se actualizeaza scorul. Daca 'hrana' ii este desenata cu verde, in cazul in care o prinde, punctajul sau se va dubla. Apoi, verific coliziunea cu peretii si cu sine insusi. Codul pentru tratarea coliziunilor este urmatorul:
void handleColisions() { //check if snake eats food if (snake[0].X == snakeFood.X && snake[0].Y == snakeFood.Y) { //increase snakeSize snakeSize++; if(score == 50) { score = score * 2; } else { score += 10; } //regenerate food spawnSnakeFood(); } //check if snake collides with itself else { for (int i = 1; i < snakeSize; i++) { if (snake[0].X == snake[i].X && snake[0].Y == snake[i].Y) { state = gameOver; } } } //check for wall collisions if ((snake[0].X < 20) || (snake[0].Y < 20) || (snake[0].X > 320) || (snake[0].Y > 240)) { delay(1000); state = gameOver; } }
O problema cu adevarat importanta este tratarea inputului de la touchscreen. Algoritmul folosit pentru a detecta schimbarea miscarii jucatorului este urmatorul: am impartit ecranul in “stanga” si “dreapta” jucatorului (in care se va muta in functie de unde a fost atins ecranul), acestea fiind date de directia si sensul sau de deplasare, presupunerea fiind ca daca doreste sa isi pastreze actuala deplasare, jucatorul nu va atinge ecranul. Aceasta abordare rezolva inclusiv problema intoarcerii in sensul invers, lucru interzis in Snake dar care se poate realiza printr-o atingere dubla a ecranului. Codul este urmatorul:
void handleInput() { TSPoint p = ts.getPoint(); pinMode(XM, OUTPUT); pinMode(YP, OUTPUT); if ((p.z > 0) && (p.z < MAXPRESSURE)) { p.x = map(p.x, TS_MINX, TS_MAXX, 0, tft.width()); p.y = 240 - map(p.y, TS_MINY, TS_MAXY, 0, tft.height()); if (snakeDir == UP) { if (p.x > snake[0].X) { snakeDir = RIGHT; } else if (p.x < snake[0].X) { snakeDir = LEFT; } else if (p.y < snake[0].Y) { snakeDir = UP; } } else if (snakeDir == DOWN) { if (p.x > snake[0].X) { snakeDir = RIGHT; } else if (p.x < snake[0].X) { snakeDir = LEFT; } else if (p.y > snake[0].X) { snakeDir = DOWN; } } else if (snakeDir == RIGHT) { if (p.y > snake[0].Y) { snakeDir = DOWN; } else if (p.y < snake[0].Y) { snakeDir = UP; } else if (p.x > snake[0].X) { snakeDir = RIGHT; } } else { if (p.y > snake[0].Y) { snakeDir = DOWN; } else if (p.y < snake[0].Y) { snakeDir = UP; } if (p.x < snake[0].X) { snakeDir = LEFT; } } } }
Partea de updatare a valorilor se face practic prin mutarea pozitiei corpului i al snake-ului catre corpul i + 1, dupa care in functie de tipul de miscare efectuata, se modifica si pozitia primului element din array care este practic cea mai importanta, celelalte fiind doar pozitia din momente de timp trecuta. Tot aici golesc spatiul unde trebuie rescris scorul. Codul este urmatorul:
void updateValues() { //update all body parts of the snake excpet the head for (int i = snakeSize - 1; i >= 0; i--) { tft.fillRect(snake[i].X, snake[i].Y, gameItemSize, gameItemSize, color); } tft.fillRect(0, 0, 150, 20, color); for (int i = snakeSize - 1; i > 0; i--) { snake[i] = snake[i - 1]; } //Now update the head //move left if (snakeDir == 0) { snake[0].X -= gameItemSize; } //move right else if (snakeDir == 1) { snake[0].X += gameItemSize; } //move down else if (snakeDir == 2) { snake[0].Y += gameItemSize; } //move up else if (snakeDir == 3) { snake[0].Y -= gameItemSize; } }
In starea de gameLost, se actualizeaza scorul maxim, se reseteaza principalele variabile globale si se trece din noua in pausedGame dupa o pauza de cateva secunde. Functia loop este doar o apelare a uneia dintre celelalte 4 functii principale in functie de valoare state-ului, la care se adauga si un delay care scade in functie de scorul atins, fapt ce genereaza o crestere constanta a dificultatii jocului.
Majoritatea problemelor au fost generate de faptul ca nu am mai folosit pana acum acest tip de LCD. Exista cateva biblioteci speciale dedicate acestui LCD (precum SPFD5408 si TouchScreen) care au insa cateva probleme pe care a trebuit sa le rezolv inainte sa incep implementarea, cum ar fi faptul ca afisarea textului se facea in oglinda. A fost nevoie sa intru si sa modific fisierul SPFD5408_Adafruit_TFT_LCD.cpp care a fost o provocare destul de mare, dat fiind ca implementarea este una destul de greoaie. De asemenea, biblioteca TouchScreen si ecranul foloseau sisteme diferite de coordonate pentru puncte (ecranul foloseste cadranul 4, iar TouchScreen foloseste cadranul 1). Aceasta problema mi-a dat multe batai de cap tocmai in etapa de preluarea a informatiei de la input, cand jucatorul meu decidea sa o ia in sensul opus celui indicat de mine. Problema s-a rezolvat prin maparea coordonatelor X si Y si schimbarea coordonatei Y in 240 - Y. De asemenea, faptul ca este nevoie sa curat eu de fiecare data ecranul pentru a putea sa afisez versiunea curenta a jocului este un amanunt obositor deoarece duce la creare de cod repetitiv care putea fi evitat, insa nu am gasit alta solutie automata.
Am folosit Arduino IDE pentru scrierea si incarcarea codului pe Arduino. Problema principala a fost lipsa functionalitatilor specifice unui IDE, precum autocomplete.
A fost o experienta interesanta sa combin diverse tipuri de cunostinte din timpul semestrului, precum si cateva dintre informatiile de la EGC din semestrul anterior. Ma bucur ca am reusit sa folosesc si cateva componente hardware si sa interactionez cu ele cu succes. De asemenea, am inteles cat de important este sa cunosti bine resursele hardware disponibile pentru a putea sa scrii un cod care sa se adapteze la cerintele pe care componentele fizice le cer si sa fie si compatibil.