Specializare: Automatică și Informatică Aplicată PROIECT DE DIPLOMĂ COORDONATOR ȘTIINȚIFIC: Șef Lucrări dr. ing. Oana Niculescu – Faida ABSOLVENT… [305857]
Universitatea Tehnică de Construcții din București
Facultatea de Hidrotehnică
Domeniul: Ingineria Sistemelor
Specializare: Automatică și Informatică Aplicată
PROIECT DE DIPLOMĂ
COORDONATOR ȘTIINȚIFIC:
Șef Lucrări dr. ing. [anonimizat]: [anonimizat]
2015
Universitatea Tehnică de Construcții din București
Facultatea de Hidrotehnică
Domeniul: Ingineria Sistemelor
Specializare: Automatică și Informatică Aplicată
PROIECT DE DIPLOMĂ
DEZVOLTAREA DE APLICAȚII GRAFICE INTERACTIVE PENTRU DISPOZITIVE MOBILE ANDROID
COORDONATOR ȘTIINȚIFIC:
Șef Lucrări dr. ing. [anonimizat]: [anonimizat]
2015
Capitolul 1. Introducere
1.1 [anonimizat].
Telefoanele mobile au devenit foarte răspȃ[anonimizat]ȃnd un mijloc de comunicare eficient și accesibil. Evoluția foarte rapidă a acestora a [anonimizat] (telefoane inteligente). Diferența dintre un telefon obișnuit și un smartphone este dată de sistemul de operare. [anonimizat]-urilor au două avantaje importante:
[anonimizat] (Application Program Interface) [anonimizat].
Al doilea avantaj major al acestui tip de sistem de operare este dat de multitasking. [anonimizat], iar trecerea de la un program la altul se face cu ajutorul unui Task Manager similar celui din sistemele de operare prezente pe calculatoarele personale.
Telefoanele mobile inteligente permit utilizatorului execuția de sarcini complexe cum ar fi: [anonimizat], citirea și organizarea e-mail-urilor personale. [anonimizat].
[anonimizat].
1.2 Descrierea problemei
Datorită acestei evoluții rapide a tehnologiei și a nevoilor oamenilor de a [anonimizat] a luat o amploare foarte mare. Acestea, [anonimizat] o [anonimizat].
[anonimizat] a sistemului de operare implementat pe dispozitivul mobil pe care rulează aceste aplicații.
Aplicațiile mobile au cunoscut o [anonimizat].
Aceste aplicații se împart în mai multe categorii:
aplicații de informare generală: [anonimizat]: vremea, [anonimizat];
aplicații cu logare folosind datele personale: sunt aplicațiile în care se cer anumite informații personale pentru autentificare, informații ce vor fi verificate cu ajutorul unor informații existente pe server;
aplicații de comunicare în rețea: sunt aplicațiile prin intermediul cărora utilizatorii comunică cu alți utilizatori la distanță;
aplicații economice: sunt aplicațiile prin intermediul cărora utilizatorii pot face achiziții de bunuri, pot realiza plăți sau alte activități de natură economică;
jocuri: sunt aplicațiile de divertisment cu ajutorul cărora utilizatorii își petrec timpul liber.
Jocurile prezintă un interes foarte mare, în special în rândul tinerilor. Datorită disponibilității telefoanelor mobile inteligente, tinerii folosesc tot mai mult timpul liber pentru jocurile de pe acest tip de dispozitive. Jocurile sunt foarte bine realizate și tind să atingă performanțele jocurile de pe calculatoare, deși resursele hardware ale unui telefon mobil sunt mult mai reduse.
Caracterul prietenos al aplicației joacă cel mai important rol pentru determinarea calității unei aplicații de divertisment. Cu cât aplicația este mai atractivă și mai ușor de utilizat cu atât calitatea acesteia este mai mare și va fi folosită de mai multe persoane.
Această lucrare de licență are ca obiectiv principal realizarea unei aplicații grafice interactive de tip joc, special optimizată pentru dispozitive mobile Android, utilizând anumite librării concepute în acest sens. Numele jocului va fi Equilibrium și va fi un joc tip 2D având ca scop ținerea în echilibru și controlarea unui obiect principal cu scopul colectării unor elemente și obținerii unui scor cât mai mare în perioada de timp impusă. Astfel, jucătorul trebuie să adune, în timpul jocului, cât încă dispune de vieți, în principal toate cutiile cu diamante ce furnizează scorul acestuia la un anumit moment dat, dar și scorul ce îl diferențiază într-un Clasamentul global.
1.3 Obiectivele lucrării
Cu toate că sunt folosite aceleași unelte pentru dezvoltare, programarea aplicațiilor pentru dispozitivele mobile este diferită, puțin mai delicată și dificilă. Dezvoltatorii care sunt conștienți de diferențele dintre calculatoarele personale și dispozitivele mobile cu privire la resursele de memorie sau puterea redusă de calcul, dezvoltă aplicații mobile mult mai eficiente care să răspundă unor provocări prezentate în cele ce urmează cu scopul îndeplinirii acestora ca și obiective principale ale lucrării.
Bateria este cel mai important punct care trebuie luat în considerare în dezvoltarea unei aplicații mobile. O aplicație cu un timp al procesorului mare va conduce la consumul rapid al bateriei, ceea ce face ca aplicația să fie inutilă. Astfel, jocul pe care mi-am propus sã îl dezvolt și să îl prezint în cele ce urmează a fost conceput în maniera în care toate elementele au fost special găndite și îmbinate astfel încât să fie optimizat și să determine un timp de procesare cât mai redus.
Deoarece procesorul dispozitivelor mobile au frecvențe mici, afectând timpul efectuării operațiilor, jocul dezvoltat de mine va încerca să comprime prin anumite tehnici timpul efectuării anumitor operații de calcul și randare.
Dimensiunea dispozitivului impune foarte multe restricții în ceea ce privește interfața utilizatorului. Se recomandă ca aplicația să ceară utilizatorului să interacționeze cu cât mai puțin text, din cauza tastaturii, considerată incomodă. De asemenea, citirea unui text cu foarte multe informații de pe un dispozitiv mobil nu este agreată. Jocul prezentat în cele ce urmează se ghidează după aceste detalii dispunând astfel de o interfață prietenoasă.
Memoria în cazul dispozitivelor mobile este calculată în megabytes. Aceasta nu poate fi mărită, ca în cazul spațiului de stocare, prin folosirea cardurilor de memorie. Aplicația nu trebuie să păstreze foarte multe date în memorie, pentru ca sistemul să ruleze în condiții normale, de aceea am implementat anumite tehnici speciale de eliberare a memorie atunci când, de exemplu, anumite elemente grafice nu mai sunt necesare.
Securitate este cea mai importantă caracteristică a unei aplicații mobile. Datorită faptului că un dispozitiv poate conține informații personale, securitatea înseamnă criptarea datelor, permisiuni pentru instalarea/dezinstalarea aplicațiilor, etc. Aplicația dezvoltată de mine va necesita permisiuni normale pentru acest segment de aplicații și va dispune de un mod securizat de criptare a datelor specifice evoluției jucătorului în cadrul jocului.
Generalitatea aplicației este o caracteristică prin care se măsoară dimensiunea grupului țintă, persoanele cărora aplicația le va fi utilă. Astfel, pe baza categoriilor din care face parte aplicația se stabilește și dimensiunea grupului tintă. De aceea am încercat ca aplicația mea tip joc să fie cât mai generală, pentru ca numărul posibililor utilizatori să crească și astfel dimensiunea grupului țintă să fie cât mai mare.
Caracterul prietenos al aplicației este o caracteristică foarte importantă mai ales pentru aplicațiile de pe dispozitivele mobile. Aceasta presupune ca aplicația să fie cât mai aproape de utilizator, să aibă o interfață cât mai ușor de utilizat și cât mai intuitivă. Aplicațiile care sunt greu de utilizat vor consuma mai mult timp pentru înțelegere și astfel utilizatorii se vor îndrepta către alte aplicații mai generale și intuitive, considerând-o pe aceasta o aplicație plictisitoare și cu o calitate scăzută. De aceea am încercat să redau aplicației mele o interfață cât mai prietenoasă prin utilizarea unor elemente special realizate pentru a fi cât mai intuitive și care se bazează pe principiul continuității. Continuitatea este caracteristică prin care aplicațiile folosesc elemente cunoscute de la alte aplicații software, astfel încât utilizatorul să nu mai depună efort pentru învățarea unei noi utilizări. Această caracteristică vine în sprijinul caracteristicii prezentate mai sus deoarece, dacă în cadrul aplicației sunt prezente elemente cunoscute deja de utilizator, caracterul prietenos al acesteia este mai mare și astfel aceasta este mai ușor de utilizat.
Portabilitatea este o caracteristică de calitate prin care se măsoară posibilitatea de rulare a aplicației pe diferite dispozitive mobile. Dispozitivele mobile prezintă o gamă foarte largă de modele și de aceea este foarte important ca o aplicație să aibă posibilitatea de a fi rulată pe un număr cât mai mare dintre acestea. Bazându-mă pe acest detaliu, am ales să-mi adresez jocul mai multor versiuni de sisteme de operare Android și astfel să asigur o omniprezență a aplicației și o continuitate în cazul în care utilizatorul își va schimba modelul de telefon.
Capitolul 2. Fundamentare Teoretică
2.1 Sistemul de operare Android
Android reprezintă un nou set de instrumente software open source pentru telefoanele mobile creat de Google și Open Handset Alliance [1]. În câțiva ani, este de așteptat să fie găsit în milioane de telefoane și alte dispozitive mobile, făcând din Android o platformă majoră pentru dezvoltatorii de aplicații. Android permite dezvoltatorilor să scrie cod în limbajul Java, controlând dispozitivul prin intermediul bibliotecilor Java dezvoltate de Google [2].
Printre punctele forte ale Android-ului comparativ cu celelalte platforme mobile se pot enumera:
arhitectură bazată pe componente: Părți dintr-o aplicație pot fi concepute și refolosite cu usurință și în alte aplicații. Se pot înlocui și reconstrui componentele existente cu versiuni proprii îmbunătățite. Aceasta va oferi dezvoltatorului posibiltatea exploatării creativității în spațiul mobil. [3]
platformă open-source. Producătorii de telefoane mobile pot folosi și customiza platforma fără a plăti bani suplimentari pentru licențe. Dezvoltatorii au avantajul cã platforma este independentă și nu este blocată în nici un furnizor care poate ajunge să fie achiziționat de către alte companii. [4]
o multitudine de servicii predefinite: Serviciul de localizare bazat pe GPS permite ulilizatorului să afle tot timpul informații despre locul în care se găsește la un moment dat. Pentru stocarea datelor Android pune la dispoziția dezvoltatorului un sistem de gestiuni de baze de date bazat pe SQLite. Pentru navigarea pe Web, Android are încorporat un browser. [5]
gestionare automată a ciclului de viață a aplicației: Programele sunt izolate unele de altele prin mai multe straturi de securitate, care vor oferi un nivel de stabilitate superior oricăror platforme mobile existente. Utilizatorul nu va trebui să-și mai facă griji despre programele ce rulează în sistem la un moment dat și nici nu va trebui să închidă anumite aplicații pentru a putea face loc altora. Platforma Android este optimizată pentru consumul redus de energie și de memorie. [6]
grafică și sunet de înaltă calitate. Pentru a reda conținutul 3D dezvoltatorul are la dispoziția sa librăria Open GL. De asemenea animațiile pot fi redate cu ajutorul flash player-ului încorporat. Pentru sunete, Android oferă codec-uri pentru o varietate mare de formate de fișiere incluzȃnd H.264 (AVC), MP3 si AAC. [6]
portabilitatea rulării pe o gamă largă de hardware curente și viitoare. Toate programele sunt scrise în Java și executate pe mașina virtuală Dalvik, existȃnd posibilitatea portării codului pe ARM, x86 și alte arhitecturi. [6]
În continuare voi prezenta elementele componente ale platformei Andoid.
2.1.1 Vedere de ansamblu
Platforma Android are o structură organizată pe mai multe nivele. Fiecare nivel component se bazează pe nivelul anterior si oferă funcții pentru nivelul imediat superior (vezi Figura 2.1).
Figura 2.1 Arhitectura Android [6]
Android este construit pe o fundație solidă: kernel-ul Linux. Creat de Linus Torvalds în 1991, în timp ce era student la Universitatea din Helsinki, Linux poate fi găsit astăzi pe o multitudine de dispozitive, de la ceasuri de mână până la supercalculatoare. Linux oferă nivelul de abstractizare hardware pentru Android care să permită portarea acestuia pe diferite arhitecturi. [1]
Pe plan intern, Android folosește Linux pentru gestionarea memoriei, managementul de procese, crearea de rețele, precum și alte servicii predefinite. Utilizatorul nu are habar de existența acestui nivel, dezvoltatorul de aplicații nu va face apeluri directe către el, dar va trebui să ia în considerare existența lui la baza arhitecturii.[3]
Următorul strat deasupra kernel-ului îl reprezintă librăriile predefinte. Aceste librării sunt scrise în totalitate în C sau C++, compilate pentru arhitectura hardware specială folosită de telefon și preinstalate de către vânzătorul telefonului. [6]
Printre cele mai importante librării implementate putem enumera următoarele:
Surface Manager: Android utilizează un manager de sistem de ferestre similar cu cel prezent în Vista sau Compiz, dar mult simplificat. În loc să fie desenate direct pe ecran, bitmap-urile sunt administrate local și combinate pentru a reda utilizatorului interfața finală. Acest sistem permite crearea unei game largi de efecte vizuale interesante, cum ar fi ferestrele transparente sau tranziția animată între ferestre.
Grafică 2D și 3D: Elementele 2D și 3D pot fi combinate într-o singurã interfață în Android. Librăria va folosi accelerația hardware dacă aceasta este prezentă pe dispozitiv sau un software renderer în caz contrar. [6]
Codecuri media: Android poate reda și înregistra audio și video într-o varietate mare de formate, inclusiv AAC, AVC (H.264), H.263, MP3 și MPEG-4. [6]
Baza de date SQL: Android include o versiune ușoară de sistem de gestiune a bazelor de date: SQLite, aceeași bază de date utilizată în Firefox și iPhone. Dezvoltatorul poate folosi acest sistem pentru depozitarea persistentă a datelor. [5]
Browser: Pentru afișarea rapidă a conținutului HTML, Android folosește o bibliotecă WebKit. Aceasta folosește același motor folosit în browser-ul Google Chrome, browser-ul Apple Safari, Apple iPhone și platforma Nokia S60. [6]
Aceste librării nu sunt aplicații de sine stătătoare, ele există doar pentru a oferi suport pentru nivelele superioare.
Android include un set de librării care susțin o mare parte din funcționalitatea pusă la dispoziție de limbajul de programare Java. Prin implementarea lor rezultă crearea unei mașini virtuale Java intitulată Dalvyk.
Fiecare aplicație Android rulează într-un proces propriu și are o instanță separată de mașină virtuală Dalvik. Dalvyk a fost astfel scrisă încȃt să permită rularea mai multor instanțe de mașină virtuală pe același dispozitiv hardware. [7]
Diferențele dintre Dalvyk si mașina virtuală Java standard[7]:
Dalvyk VM rulează pe fișiere .dex care sunt obținute prin covertirea fișierelor .class și .jar în momentul compilării. Fișierele .dex sunt mai compacte și mai eficiente decât fișierele .class pentru a optimiza consumul de memorie și de energie.
Bibliotecile de bază Java care vin cu Android sunt diferite atât față de bibliotecile din Java Standard Edition (Java SE) și de cele din Java Mobile Edition (Java ME). Există totuși un subset comun de API-uri.
Peste nivelul librăriilor native și a runtime-ului Android se găsește nivelul de Application framework. Acest nivel este folosit de către dezvoltator pentru a scrie aplicații. Android vine cu o serie de componente predefinite, dar utilizatorul are posibilitatea de a extinde aceste componente și de a crea unele noi.
Principalele componente ale framework-ului[8]:
Un set bogat de componente UI ce pot fi folosite de utilizator pentru a crea o aplicație. Acest set include grid-uri , listview-uri , textbox-uri etc.
Activity Manager: controlează ciclul de viață al aplicațiilor și modul de comunicare dintre acestea.
Resource Manager: controlează accesul la resursele non-cod, cum ar fi: string-uri, layout-uri, imagini etc.
Content providers: aceste obiecte încapsulează datele și informațiile care trebuie împărțite între aplicații.
Notification Manager: permite notificarea user-ului sub diferite forme.
2.1.2 Fundamentele aplicațiilor Android
Aplicațiile Android sunt scrise în limbajul de programare Java. Codul compilat împreună cu resursele statice (imagini, texte etc.) sunt stocate cu ajutorul unui instrument într-un pachet Android cu extensia .apk.
De obicei fiecare aplicație rulează într-un proces Linux separat. Acest proces este automat creat de Android cȃnd este nevoie să se execute codul aplicației și este oprit cȃnd nu mai este nevoie de el sau în momentul în care trebuie eliberate resursele sistemului. Fiecare proces are propria sa mașină virtuală, astfel că aplicațiile Android rulează izolat una față de cealaltă. [7]
Un alt avantaj al aplicațiilor Android este acela că o aplicație poate folosi elemente create de altă aplicație. De exemplu, dacă o aplicație trebuie să afișeze o listă de imagini, iar o altă aplicație are deja definită o astfel de componentă și a făcut-o publică pentru utilizare, componenta respectivă poate fi refolosită excluzȃnd nevoia de a fi dezvoltată din nou. Aplicația nu trebuie sa încorporeze codul respectiv, ci doar să facă un apel către rutina corespunzătoare din aplicația deja existentă.
Aplicațiile Android sunt formate din componente bine cuplate, legate printr-un manifest de proiect care descrie fiecare componentă și modul în care acestea interacționează.
Android oferă un set complet de API-uri și instrumente pentru a permite dezvoltarea și testarea eficientă a aplicațiilor. Pentru a începe dezvoltarea pe Android trebuiesc instalate librăriile esențiale JDK (Java Development Kit) și Android SDK (Standar Development Kit). Utilizatorul are posibilitatea integrării cu un mediu de dezvoltare existent (Eclipse, Idea etc.) și posibilitatea testării aplicațiilor pe un emulator ce simulează un dispozitiv mobil cu Andorid OS.
Emulatorul Android imită toate caracteristicile hardware și software ale unui dispozitiv mobil tipic, cu excepția faptului că nu se pot primi sau efectua apeluri telefonice reale și nu se poate simula folosirea camerei foto încorporate. Se pune la dispoziția utilizatorului o tastatură și un ecran care se pot accesa folosind mouse-ul sau tastatura calculatorului. Se pot afișa și utiliza toate aplicațiile disponibile în sistemul Andorid, se pot utiliza configurații hardware și versiuni de platformă diferite. Odată pornit, emulatorul poate invoca alte aplicații sau servicii, poate reda conținut media și poate să se conecteze la Internet [9]. Figura 2.2 prezintă ecranul principal al emulatorului:
Figura 2.2 Emulator Android
Android este bine integrat în mediul de dezvoltare Eclipse cu ajutorul unui plug-in dezvoltat de Google denumit ADT(Android Development Tools). Acest instrument oferă suportul necesar pentru crearea, rularea și testarea aplicațiilor, conține un set bogat de editoare utile și este integrat cu toate instrumentele de dezvoltare prezente în platforma Android. Permite de asemenea exportarea proiectelor în pachete .apk care pot fi instalate ulterior pe o platformă mobilă reală.
Eclipse împreună cu ADT reprezintă mediul de dezvoltare recomandat, dar se pot folosi și altele (InteliJ, Emacs etc.), platforma Android SDK oferind toate instrumentele necesare dezvoltării de aplicații.
2.2 Multi-platforma LibGDX
2.2.1 Noțiuni generale
Libgdx este o multi-platformă open-source destinată dezvoltării aplicațiilor software de tip jocuri utilizând limbajul de programare JAVA. Aceasta permite dezvoltarea de aplicații pentru Windows, Mac, Linux, Android, iOS și HTML5, toate platformele prezentate împărțind doar un cod de bază comun scris în limbajul de programare JAVA la care se atașează un cod specific fiecărei platforme. [13]
Pentru dezvoltatorii de jocuri, este esențial să dispună de toate instrumentele care oferă fundamentele ce permit crearea rapidă de prototipuri și implementarea eficientă a ideilor acestora. Acest lucru se realizează când Libgdx intră în joc.
Libgdx permite scrierea codului o dată și implementarea pe mai multe platforme fără modificări. În loc să așteptăm pentru ca cele mai recente modificări sa fie descărcate pe dispozitivul disponibil sau să fie compilate pentru HTML5, ne putem bucura de un mod extrem de rapid de codare a aplicației dezvoltate în primul rând într-un mediu desktop. Putem utiliza toate instrumentele sistemului Java pentru a fi cât mai productivi reducând timpul de repetiții în implementarea anumitor idei sau timpul de găsire și reparare a diverselor erori. Astfel, putem opta în utilizarea anumitor funcționalități comode oferite de Java Virtual Machine (JVM), cum ar fi Code Hot Swapping, ce permite afișarea imediată a efectului produs de codul modificat rulat la momentul execuției. [11], [12]
Libgdx permite să coborâm la un nivelul cât mai de bază dorit, oferindu-ne acces direct la sisteme de fișiere, dispozitive de intrare, dispozitive audio și OpenGL printr-o interfață unificată OpenGL ES 2.0 respectiv 3.0.[10]
În plus față de aceste beneficii, LibGDX reprezintă până la urmă un set complet de API-uri care ajută la dezvoltarea jocurilor cu următoarele facilități: randare și text, creare de interfețe utilizator, redare de efecte sonore și de muzică, algebră și trigonometrie, JSON și XML și așa mai departe.
În cazul în care este necesar, Libgdx lasă domeniul Java și se focusează pe cod nativ pentru a obține cea mai bună performanță posibilă. Toată această funcționalitate este ascunsă în spatele API-urilor Java, astfel încât dezvoltatorii de aplicații să nu își facă griji cu privire la compilarea codului nativ pentru toate platformele. [12]
Libgdx (vezi Figura 2.3), ca și platformă de dezvoltare a jocurilor bazate pe Java, oferă un acces unificat la un strat comun pentru toate platformele suportate. Libgdx de asemenea, face uz de C/ C ++ pentru a obține sprijin pe partea de multi-platformă, pentru a integra alte librării scrise in limbajul C precum și pentru a stimula performanța aplicațiilor pentru sarcini critice. Mai mult, arhitectura abstractizează natura complexă a platformelor suportate unificându-le într-un API (Application Programming Interface) comun.[13]
Un aspect important este deci de a înțelege că Libgdx este o arhitecturã și nu un motor de joc care, de obicei, vine cu o mulțime de instrumente, dar și cu un flux de lucru complet predefinit. Acest lucru ar putea suna ca un dezavantaj la început, dar de fapt, se dovedește a fi un avantaj care permite dezvoltatorilor să defineascã în mod liber propriul flux de lucru pentru fiecare proiect. De exemplu, Libgdx va permite dezvoltatorilor de aplicații să meargă la un nivel scăzut, astfel încât ar putea adăuga propriile apeluri OpenGL dacă este într-adevăr necesar la un moment dat. Cu toate acestea, de cele mai multe ori ar trebui să fie suficient rămânerea la un nivel înalt și să se utilizeze funcționalitățile implicite ale Libgdx.[12]
Figura 2.3 Prezentare LibGDX
2.2.2 Caracteristici esențiale ale arhitecturii
Grafică:
Redare prin OpenGL ES 1.x și 2.0 pe toate platformele;
Legături personalizate dintre OpenGL ES 2.0 și versiuni de Android de la 2.0 în sus;
Ajutor de nivel scăzut pentru OpenGL:
Tablouri și obiecte tampon de tip vertecsi;
Mesh-uri: Un Mesh deține noduri formate din atribute specificate de o instanță VertexAttributes (Atributele vertecsilor). Vertecsii sunt organizați fie în VRAM în formă de obiecte tampon vertex sau RAM sub formă de vector vertex. Mesh-urile sunt gestionate automat. În cazul în care contextul OpenGL a pierdut toate obiectele tampon vertex, atunci trebuie reîncărcate când contextul este recreat. Acest lucru se întâmplă doar pe Android, atunci când un utilizator trece la o altă aplicație sau primește un apel. Un mesh va fi reîncărcat în mod automat. Un Mesh este format din vertecsi și opțional din indici, care specifică ce noduri definesc un triunghi. Fiecare vertex este compus din atribute, cum ar fi poziția, culoarea sau coordonatele texturii. [13]
Texturi: O textură poate fi gestionată și în cazul în care contextul OpenGL este pierdut toate texturile gestionate ajung nevalidate. Acest lucru se întâmplă atunci când un utilizator trece la o altă aplicație sau primește un apel. Texturile gestionate se reîncărcă automat. O textură trebuie să fie eliminată atunci când nu mai este utilizată.[10]
Obiecte framebuffer (numai GLES 2.0) – Framebuffer-ele sunt gestionate, iar în cazul unei pierderi de context OpenGL, care se întâmplă pe Android, atunci când un utilizator trece la o altă aplicație sau primește un apel, la revenire va fi recreat framebuffer-ul în mod automat. Un framebuffer, trebuie eliminat în cazul în care nu mai este necesară existența lui. [11]
Shadere: Un program shader încapsulează o pereche vertex și fragment de shader legate pentru a forma un program de shader utilizabil cu OpenGL ES 2.0. După construcție, un ShaderProgram poate fi utilizat pentru a desena un Mesh. Pentru a face unitatea de procesare vizuală (GPU) să utilizeze un ShaderProgram specific, programele vor începe în metoda de begin() cu legarea efectivă de program. Când un ShaderProgram este legat se pot seta atributele nodurilor și atribute necesare prin metodele respective. Un ShaderProgram poate fi nelegat (dereferențiat) cu un apel spre metoda end(). Un ShaderProgram trebuie eliminat printr-un apel a metodei dispose() atunci când nu mai este necesară instanța acestuia. ShaderProgram-ele sunt gestionate, iar în cazul în care contextul OpenGL este pierdut toate shaderele devin nevalidate și trebuie să fie reîncărcate. Acest lucru se întâmplă pe Android atunci când un utilizator trece la o altă aplicație sau primește un apel. ShaderProgram-ele gestionate sunt reîncărcate automat atunci când contextul OpenGL este recreat, astfel încât nu trebuie să facem asta manual.[11]
Modul de redare imediată de emulare;
Randare a formelor simple: Randează puncte, linii, contururile formelor și forme pline. În mod implicit o proiecție ortografică 2D, se folosește cu originea în colțul din stânga jos și unitățile sunt specificate în pixeli ai ecranului. Acest lucru poate fi schimbat prin configurarea matricei de proiecție, de obicei, folosind matricea Camera.combined. Dacă rezoluția ecranului se schimbă, matricea de proiecție trebuie să fie actualizată. Formele sunt randate în loturi pentru a crește performanța. [12]
Software automat sau hardware automat pentru noua generație mip-map.
Suport ETC1 (nu este disponibil în JavaScript backend): pentru codarea și decodarea ETC1 a imaginilor comprimate.
API-uri 2D de nivel înalt:
Librărie specializată pentru manipulare bitmap pe partea de CPU ( ): Un Pixmap reprezintă o imagine în memorie. Ea are o lățime și o înălțime exprimată în pixeli, formatul precizând numărul și ordinea de componente de culoare per pixel. Coordonatele pixelilor sunt specificate în raport cu colțul din stânga sus al imaginii, cu axa x spre dreapta și axa y îndreptatã în jos. Un Pixmap stochează datele în memorie heap nativă. Este obligatoriu să apeleze metoda dispose(), atunci când nu mai este necesară existența pixmap-ului, în caz contrar vor rezulta pierderi de memorie; [13]
Cameră cu proiecție ortografică;
Sprite batching de înaltă performanță: Din punct de vedere tehnic, "batching" reprezintă punerea unor acțiuni multiple și a datele lor într-o structură de date astfel încât să poată fi executate mai degrabă simultan decât individual. Cea mai mare piedică a GPU (Graphics processing unit – Placa Video) moderne nu este puterea lor de lucru, ci mai degrabă comunicarea între jocul care rulează pe CPU și GPU (Graphics processing unit – Placa Video). Fiecare pachet de date trimis către GPU (Graphics processing unit – Placa Video) produce o supraîncărcare, iar un pachet de dimensiuni mici produce o supraîncărcare care este la fel de mare ca cea a unui pachet de dimensiuni mari; așa că reprezintă un câștig mare, atunci când se trimite un pachet mare, mai degrabă, decât o sută de pachete mai mici. Pe scurt, batching-ul produce un pachet mare din toate acele pachete mici. Batching-ul este deosebit de util pentru sprite-uri (imagine grafice), din cauza cantității mici de date pe care le posedă. [11], [12]
SpriteCache – Desenează imagini 2D, optimizate pentru geometrie care nu se schimbă. Sprite-urile și/sau texturile sunt memorate în cache având o identitate (id). Ele pot fi folosite mai târziu pentru desenare. Mărimea, culoarea și textura regiunii pentru fiecare imagine cache nu pot fi modificate. Aceste informații sunt stocate în memoria video și nu trebuie să fie trimise cãtre GPU (Graphics processing unit – Placa Video) de fiecare dată când se desenează. Pentru salvare în memoria cache a sprite-urilor sau texturilor, prima dată se apelează beginCache(), apoi se apelează metoda de adăugare corespunzătoare pentru a defini imagini. Pentru a completa memoria cache se apelează endCache() și se păstrează valoare id returnată din memoria cache. În mod implicit, SpriteCache desenează folosind coordonatele ecranului și utilizează axa OX îndreptată spre dreapta, axa OY îndreptată în sus cu originea în colțul din stânga jos a ecranului. Matricele implicite pentru transformare și proiecție pot fi modificate. Dacă este redimensionat ecranul, matricea de SpriteCache trebuie actualizată. Deoarece SpriteCache este un obiect destul de mare, de obicei numai o instanță ar trebui să fie utilizată pentru o aplicație întreagă. SpriteCache funcționează cu OpenGL ES 1.x și 2.0. Pentru 2.0, folosește propriul shader personalizat pentru a desena. [12], [13]
Atlase de textură cu suport pentru spații de separare, generate offline sau online: un atlas de textură este o imagine mare care conține o colecție de sub-imagini, fiecare dintre acestea fiind o textură pentru o parte a unui obiect 2D sau 3D. [10]
Fonturi bitmap: pot fi generate offline sau încărcate din fișiere TTF. Fontul este format din 2 fișiere: un fișier imagine sau TextureRegion ce conține simbolurile și un fișier în format text, AngleCode BMFont care descrie unde este fiecare simbol în imagine. În prezent, doar o singură imagine de simboluri este suportată. Textul este redactat cu ajutorul unui Batch. Textul poate fi memorat în cache în BitmapFontCache pentru o randare mai rapidă a textului static și elimină astfel nevoia de a calcula localizarea fiecărui simbol cadru cu cadru (frame by frame). Textura pentru BitmapFont încărcată dintr-un fișier este gestionată. Metoda dispose() trebuie să fi apelată pentru a elibera textura atunci când nu mai este necesară. Un BitmapFont încărcat cu ajutorul unui TextureRegion este gestionat în cazul în care și textura din regiune este gestionată. Eliminarea din memorie a BitmapFont-ului va avea ca efect eliminarea texturii regiunii, care nu ar trebui să fie de dorit în cazul în care textura este încă folosită în altă parte.[12]
Sistem de particule 2D (2D Particle system)
Bibliotecă 2D pentru interfața de utilizare, bazat pe scene, dispunând pe deplin de posibilitatea personalizării.
API-uri 3D de nivel înalt:
Cameră cu proiecție perspectivă;
Batching Decal, pentru panouri 3D sau sisteme de particule: Un decal este un tip primitiv, care proiectează un material pe suprafețe dintr-o scenă. 12]
Încărcătoare de bază pentru Wavefront OBJ și MD5;
API pentru randare 3D cu materiale, sistem de iluminare și suport pentru modelele de încărcare;
Utilități
Colecții personalizate, cu suport primitiv;
Modul de sciere și citire JSON (JavaScript Object Notation) cu suport pentru deserializare;
Modul de scriere și citire XML (Extensible Markup Language);
Instrumente
Editor de particule;
Program de compresie a texturii;
Generator de font bitmap;
Audio
Streaming de muzică și redare efecte de sunet WAV, MP3 și OGG: O instanță de Muzică – reprezintă un flux al unui fișier audio. Interfața suportă întreruperea, reluarea și așa mai departe. Instanțele de Muzică sunt întrerupte și reluate automat când aplicația este reluată. Un efect de sunet este un clip audio scurt, care poate fi redat de mai multe ori în paralel. Este complet încărcat în memorie, astfel se recomandă încărcarea doar a fișierelor audio de dimensiuni mici. [11]
Acces direct la dispozitiv audio pentru redarea și înregistrare
Manevrarea intrării (Input Handling)
Detecție intrare pentru mouse, touch-screen, tastatură, accelerometru și busolă;
Detector de gesturi;
Fizică și Funcții Matematice
Matrice, vector. Operații pe matrici și vectori sunt accelerate prin cod nativ C în cazul în care este posibil;
Delimitarea formelor și volumelor;
Clase Frustum: O piramidă dreptunghiulară trunchiată. Utilizată pentru a defini zona vizibilă și proiecția acesteia pe ecran;
Interpolatori comuni: preiau o valoare liniară în intervalul de 0-1 și furnizează (de obicei) valoarea neliniară interpolată;
Testarea intersecției și suprapunerii dintre diferite obiecte geometrice;
Fișier intrare/ieșire și stocare:
Sistem abstract de administrare fișiere;
Suport fișiere binare pentru Javascript backend
Preferințe de dimensiuni mici pentru depozitarea setărilor;
Capitolul 3. Cerințe preliminare de instalare și configurare
Înainte de a putea începe să realizăm orice aplicație sau joc, trebuie să descărcăm și să instalăm biblioteca Libgdx și, de asemenea, unele software-uri suplimentare.
3.1 Setarea mediului de dezvoltare Java
Primul pas va fi de a instala Kit-ul de Dezvoltare Java (Java Development Kit – JDK). Pentru toate instrumentele, aplicațiile și SDK-urile (Software Development Kit) pe care le voi folosi, există versiuni diferite pentru Windows și Mac. Browser-ul va detecta automat sistemul de operare utilizat și va redirecționa la descărcarea relevantă a versiunii, dar cu toate acestea trebuie să ne asigurăm verificând dacă descărcăm versiunea corectă. De asemenea, există versiuni diferite pentru Procesoare pe 32 de biți și 64 de biți și de asemenea trebuie să verificăm dacă descărcăm versiunea corectă pentru configurația calculatorului utilizat la dezvoltare.
Putem descărca gratuit cea mai recentă versiune a JDK (Java Development Kit) de pe site-ul Oracle: http://www.oracle.com/technetwork/java/javase/downloads/index.html
La dezvoltarea prezentei aplicații grafice interactive pentru dispozitive mobile am utilizat cea mai recentă versiune JDK 8u5, iar pentru instalare am parcurs următorii pași:
Am deschis browser-ul web și am navigat către website-ul menționat (vezi Figura 3.1.)
:
Figura 3.1 Accesare website oficial Oracle
Am dat click pe butonul DOWNLOAD pentru a începe descărcarea ultimului JDK. Este important să se aleagă JDK în locul pachetului JRE deoarece pachetul JDK conține Java Runtime Environment (JRE) utilizat pentru a rula aplicații Java și orice aspect care este necesar pentru a le dezvolta. După aceasta a trebuit să accept acordul de licență și să aleg versiunea care este potrivită pentru platformã mea.
Figura 3.2 Descărcare Java Development Kit
Pentru a instala JDK, pur și simplu am rulat fișierul de instalare descărcat (în cazul meu, JDK-8u5-windows-i586.exe) și am urmat instrucțiunile de pe ecran.
Pe ecranul de bun venit al programului de instalare, am dat click pe butonul Next pentru a continua (vezi Figura 3.3):
Figura 3.3 Instalre Java Development Kit
Apoi, am păstrat toate caracteristicile selectate pentru a fi instalate, și am apăsat din nou pe butonul Next pentru a continua.
Odată ce instalarea a fost completă, am dat click pe butonul Close pentru a ieși din programul de instalare.
3.2 Setarea Mediului Integrat de Dezvoltare ECLIPSE
Următorul pas a fost descărcarea și instalarea programului Eclipse, un Mediu Integrat de Dezvoltare (IDE) disponibil gratuit și open source pentru dezvoltarea de aplicații scrise în limbajul de programare Java. Am accesat următoarea adresă: http://www.eclipse.org/downloads/ și am ales Eclipse IDE pentru dezvoltatori Java, așa cum se poate observa în captura de ecran din Figura 3.4:
Figura 3.4 Descărcare Mediu Integrat de Dezvoltare Eclipse
După ce descărcarea s-a terminat, am extras conținutul arhivei (aplicația Eclipse).
3.3 Descărcarea LibGDX
Am accesat următoarea adresă: http://libgdx.badlogicgames.com/releases/ și am optat, pentru a descărca LibGDX fișierul libgdx-1.2.0.zip, cea mai recentă versiune stabilă.
Figura 3.5 conține o listă cu toate fișierele disponibile:
Figura 3.5 Descărcare Framework Libgdx
După ce descărcarea s-a terminat, am extras conținutul arhivei – programul extras ce prezintă o interfață grafică intuitivă mă va ajuta mai târziu la crearea unui nou proiect tip joc.
3.4 Instalarea Android SDK
Sistemul de operare Android este unul dintre platformele suportate de LibGDX. Înainte de a putea crea aplicații Android, va trebui descărcat și instalat Android SDK.
Am navigat la http://developer.android.com/sdk/index.html și am apăsat pe butonul Download the stand-alone Android SDK Tools for Windows, așa cum se poate observa în Figura 3.6 În cazul în care aș fi utilizat un sistem de operare, altul decât Windows, trebuia să derulez lista mai jos, după care să apăs pe butonul de descărcare pentru alte platforme și să aleg platforma corespunzătoare.
Figura 3.6 Descărcare Android Software Development Kit
După ce descărcarea s-a terminat, am executat programul de instalare (de exemplu, installer_r22.0.4-windows.exe) și am început să urmez instrucțiunile de pe ecran.
Atunci când am încercat să instalez SDK Android am văzut ecranul din Figura 3.7 Acest lucru se datorează faptului că programul de instalare nu poate găsi JDK, deși l-am instalat deja.
Figura 3.7 Instalare Android Software Development Kit
Așadar trebuie să setez valoarea specifică de mediu JAVA_HOME către calea de instalare a JDK. Pentru a găsi calea corectă, am mers la C:\Program Files\Java\ din interiorul căruia am copiat denumirea folder-ului ce începea cu JDK si pe care am alăturat-o la calea completă, așa cum se vede în Figura 3.8.:
Figura 3.8 Localizare dosar Java Development Kit
Calea completă va arăta acum astfel C:\Program Files\Java\jdk1.8.0_05. Acum trebuie să setez variabila specifică de mediu. Am dat click dreapta pe Computer, apoi click pe Properties pentru a deschide fereastra de sistem a panoului de control.
Am dat click pe setări complexe de sistem în partea stângă a ferestrei, așa cum se vede în Figura 3.9:
Figura 3.9 Accesare setări avansate ale sistemului
Va apărea fereastra de Proprietăți a Sistemului după care se dã click pe butonul Variabile de Mediu (vezi Figura 3.10.):
Figura 3.10 Accesare variabile specifice de mediu
Va apărea fereastra cu variabilele de mediu. Am dat click pe butonul New (în partea de sus), care corespunde variabilelor pentru utilizator, așa cum se vede în Figura 3.11:
Figura 3.11 Setarea unei noi variabile specifice de mediu
Va apărea o fereastră cu titlul New User Variable. Acum, voi completa cele două câmpuri tip text. Am introdus JAVA_HOME în câmpul Denumirea variabilei (Variable name) și calea JDK găsită mai devreme în câmpul valoric aitrbuit variabilei (Variable value), așa cum se observă în Figura 3.12.:
Figura 3.12 Setarea variabilei specifice de mediu Java
Acum, sistemul este pregătit pentru revenirea la programul de instalare SDK Android. Programul de instalare SDK Android, dacă este încă în desfășurare, trebuie închis, pentru a permite ca modificarea să intre în vigoare.
Acum, revenind la instalarea Android SDK, am dat click pe Next pentru a continua instalarea.
În Figura 3.13 se cere alegerea utilizatorilor pentru care Android SDK trebuie instalat. De obicei, sugestia privind instalarea pentru oricine folosește acest calculator este perfect în regulă, așa că am dat click pe Next pentru a continua.
Figura 3.13 Setarea permisiunilor pentru Android Software Development Kit
Acum, se alege locația de instalare în calculator. Se poate păstra în condiții de siguranță locația sugerată și apăsa pe Next pentru a continua.
După aceasta, se va cere alegerea unui folder în meniul Start. Din nou, se poate să păstrăm sugestia și să apăsăm pe Next pentru a porni procesul de instalare.
După finalizarea instalării, am dat click pe Next pentru a continua.
Odată ce instalarea s-a terminat, se va opta pentru pornirea Manager Android SDK care permite descărcarea „imaginilor de sistem” pentru nivelurile API specifice pe care dorim să dezvoltăm aplicații.
Acum, trebuie aleasă cel puțin versiunea Android 2.2 (API 8) și/ sau orice alte niveluri API superioare și se apasă pe butonul Install pentru a descărca și instala automat toate fișierele relevante, așa cum se observă în Figura 3.14 Motivul pentru care doresc să utilizez cel puțin nivelul API 8 este că versiunile anterioare de Android 2.2 nu suportă OpenGL ES 2.0, de care voi avea nevoie în realizarea jocului. Folosirea unui anumit nivel API îmi permite controlul asupra unei game de dispozitive care vor fi capabile să vadă și să instaleze jocul meu prin intermediul Google Play Store.
Figura 3.14 Configurare Android Software Development Kit
După ce procesul de descărcare și instalare se termină, se poate închide fereastra Android SDK Manager.
3.5 Rulare Eclipse și instalare plugin-uri
După rularea mediului integrat de dezvoltare Eclipse și după setarea locației spațiului de lucru apare vizualizarea Eclipse, care este, de asemenea, numită perspectiva Java. În partea stângă, se află secțiunea Package Explorer (vezi Figura 3.15).
Figura 3.15 Perspectiva Mediului integrat de Dezvoltare Eclipse
Pentru a instala plugin-uri noi, am mers la bara de meniu, și am apăsat pe Ajutor, apoi am dat click pe Install New Software. Se deschide fereastra Install, unde există posibilitatea tastării URL-urilor speciale de depozitare pentru a căuta noi plugin-uri. Google oferă o listă de astfel de adrese URL la https://developers.google.com/eclipse/docs/getting_started. A trebuit să aleg adresa URL corespunzãtoare instalãrii Mediului integrat de Dezvoltare Eclipse. Deoarece Eclipse 4.3.2 (Kepler) a fost cea mai recentă versiune disponibilă, potrivit Google, adresa URL sugerată pentru versiunea mea este http://dl.google.com/eclipse/plugin/4.3.
Se introduce URL-ul în câmpul de tip text care este etichetat Work with și se apasă Return pentru a permite Mediului integrat de Dezvoltare Eclipse să realizeze o solicitare pentru o listă de descărcări disponibile. Am selectat totul din lista care este afișată în Developer Tools în vederea adăugãrii suportului pentru aplicații Android. În cele din urmă, am selectat Google Web Toolkit SDK 2.5.1 în SDK conform Figurii 3.16 în vederea adăugãrii suportului pentru aplicații HTML5 / GWT și am apăsat pe Next pentru a continua:
Figura 3.16 Instalare Google Web Toolkit Plugin
Când descărcarea este terminată, Eclipse va afișa un avertisment de securitate pentru a mă anunța că sunt pe cale de a instala conținut nesemnat și dorește să știe dacă ar trebui să continue sau să abandoneze. Există întotdeauna un potențial risc de a instala software rău intenționat. Cu toate acestea, în acest caz, descărcarea este furnizată de Google, o companie cunoscută și demnă de încredere. Am dat click pe butonul OK acceptând avertismentul și continuând instalarea. După terminarea instalării, este necesară o repornire finală a Mediului integrat de Dezvoltare Eclipse.
Acum, a trebuit să instalez plugin-ul Gradle pentru Eclipse (vezi Figura 3.17), astfel încât să pot importa proiectul în Eclipse prin Gradle. Pentru aceasta, (s-au urmat pașii anteriori din nou) am reluat pașii anteriori, utilizând de data aceasta adresa http://dist.springsource.com/release/TOOLS/gradle.
Figura 3.17 Instalare Gradle Plugin
Capitolul 4. Specificațiile și arhitectura sistemului
Acest capitol va prezenta arhitectura conceptuală a sistemului, cerințele funcționale descrise sub forma cazurilor de utilizare și cerințele non-funcționale care specifică atributele sistemului și sunt corelate cu cerințele funcționale.
4.1 Cerințele aplicației
Cerințele aplicației reprezintă ceea ce utilizatorul (se) așteaptă de la sistem, și anume, capabilități și constrângeri la care un sistem trebuie să se conformeze. Identificarea cerințelor este pasul cel mai important în dezvoltarea sistemului, pentru a putea finaliza cu succes aplicația.
Cerințele prezentate în acest subcapitol descriu funcționalitățile sistemului care facilitează și eficientizează experiența de joc (vezi Tabelul 4.1). O parte din cerințele sistemului se regăsesc la majoritatea aplicațiilor de acest tip, însă există și alte funcționalități care diferențiază prezentul joc de ceea ce există momentan pe piață.
Tabel 4.1 Sumar al cerințelor funcționale de bază
4.1.2. Cerințele non-funcționale
Spre deosebire de cerințele funcționale, cele non-funcționale reprezintă proprietăți ce impun constrângeri asupra sistemului (vezi Tabelul 4.2). Acestea specifică atributele sistemului și nu ceea ce trebuie să facă sistemul.
Tabel 4.2 Sumar al cerințelor non-funcționale
Cea mai importantă cerință non-funcțională a aplicației este dată de caracterul prietenos al aplicației și presupune ca aplicația să fie cât mai apropiatã utilizatorului, să aibă o interfață intuitivă cât mai ușor de utiliza, toate la un loc necesitând un timp al procesorului cât mai scăzut. Aplicațiile care sunt greu de utilizat vor consuma mai mult timp pentru înțelegere și astfel utilizatorii se vor îndrepta către alte aplicații mai intuitive, considerând-o pe aceasta o aplicație cu o calitate scăzută.
Continuitatea, o sub-funcționalitate ce completează caracterul prietenos al unei aplicații, este caracteristica prin care aplicațiile folosesc elemente cunoscute de la alte aplicații software, astfel încât utilizatorul să nu mai depună efort pentru învățarea utilizării. Această caracteristică vine în sprijinul caracteristicii prezentate mai sus deoarece dacă în cadrul aplicației sunt prezente elemente cunoscute deja de utilizator, caracterul prietenos al acesteia este mai mare și astfel aceasta este mai ușor de utilizat. Astfel, utilizatorul când va avea prima interacțiune cu aplicația, va regăsi un meniu intuitiv cu elemente bine definite, cunoscute ca și functionalități deja de la alte aplicații de acest tip destinate telefoanelor mobile. Simplitatea meniului reprezintă o caracteristică de luat în seamă, deoarece utilizatorul dorește să aibă parte de o experiență în aplicație cât mai relaxantă și ușoară care să nu dispună de elemente mult prea complexe (de exemplu, pentru a începe un joc nou să nu fie nevoie de foarte mulți pași). Experiența de joc, trebuie de asemeni să se focuseze pe aceste caracteristici deoarece, un utilizator se va plictisi foarte repede de un joc dacã nu are un scop bine definit, o interfață în timpul jocului care să îl facă să se simtă relaxat, atent, focusat și care sã îl ghideze cu ușurință în îndeplinirea scopului său.
Utilizabilitatea descrie ușurința cu care un sistem poate fi învățat și utilizat. Este o cerință non-funcțională la fel de importantă ca și cele funcționale, deoarece aceasta conduce la creșterea numărului de utilizatori ai aplicației, în detrimentul altor aplicații de același tip.
Suportabilitatea, performanța și disponibilitatea se află și ele printre cele mai importante cerințe non-funcționale ale sistemului.
Deoarece utilizatorii aplicației nu pot fi instruiți înainte de folosirea aplicației, utilizabilitatea devine o caracteristică critică a sistemului.
Odată ce utilizatorul a început jocul, chiar dacă părăsește aplicația pentru o anumită perioadă de timp sau are parte de o acțiune exterioară aplicației, cum ar fi primirea unui apel telefonic, sesiunea va fi reținută iar jocul va intra în mod automat în modul de pauză, utilizatorul nefiind nevoit să reînceapă o sesiune nouă de joc, ci va putea sã revenã în aplicație putând sã continue jocul de unde a rămas. Această funcționalitate este o caracteristică de utilizabilitate, scutind utilizatorul de efectuarea unor pași inutili.
Mentenabilitate se referă la abilitatea sistemului de a fi ușor de modificat și întreținut pentru a putea integra ușor noi funcționalități sau a putea modifica unele funcționalități deja existente. Partea de poveste a jocului trebuie structurată astfel încât să permită adăugarea de noi moduri de joc, modul existent fiind gândit pentru viitoare îmbunătățiri, de exemplu partea de generare a obiectelor trebuie structurată pe module și trebuie să respecte șabloanele de design pentru a putea introduce alte obiecte cu diverse atribute în viitor.
Cerințele de performanță se referă de obicei la timpul de răspuns al aplicației. Pentru îndeplinirea acestei cerințe am utilizat anumiți algoritmi de optimizare dar și anumite funcții și metode deja predefinite, toate la un loc având ca scop micșorarea timpului de procesare a informațiilor în procesor și reducerea timpului acordat randării elementelor din partea plăcii video.
Disponibilitatea descrie măsura în care sistemul trebuie să funcționeze pentru utilizatori. Astfel, jocul meu poate fi accesat mereu, fiind pregătit în orice moment pentru începerea unei noi sesiuni de joc, chiar dacă în paralel are loc repararea erorilor ce pot interveni.
4.2 Modelul cazurilor de utilizare
Modelul cazurilor de utilizare este definit de către Procesul Unificat în cadrul disciplinei de Cerințe și cuprinde întregul set de cazuri de utilizare. Este un model al funcționalităților sistemului și al mediului în care sistemul este utilizat.
Un scenariu este o secvență specifică de acțiuni și interacțiuni între actori și sistem. Se mai numește și instanță de caz de utilizare.
Un caz de utilizare este o colecție de scenarii de succese și eșecuri care descriu actorii utilizând sistemul pentru a atinge un anumit scop. Cazurile de utilizare ale prezentei aplicații, denumită Equilibrium, vor fi captate atât sub formă scrisă, cât și sub formă de diagramă a cazurilor de utilizare.
4.2.1 Diagrama cazurilor de utilizare
Diagrama UML a cazurilor de utilizare ilustrează numele acestora și ale actorilor sistemului, împreună cu relațiile dintre aceștia.
Actorul este un obiect cu un comportament specific (persoană, sistem computerizat). Actorul sistemului Equilibrium este utilizatorul jocului.
Pe diagrame sunt prezentate acțiunile dintre variantele posibile de utilizare. Diagrama reflectă cerințele către sistem din punct de vedere al utilizatorului.
Variantele de utilizare sunt funcțiile efectuate de sistem cerute din partea persoanelor cointeresate de sistemele elaborate. Diagrama arată că persoana inițiază diagrama variantelor de utilizare. La fel din ea se vede că persoanele cointeresate primesc datele de la variantele de utilizare. Din diagramele variantelor de utilizare se pot afla multe informații referitoare la sistem. Acest tip de diagramă descrie funcționarea sistemului la general. Utilizatorii, managerii proiectării, analiticii, specialiștii și toți cei care sunt cointeresați în sistemul dat pot să înțeleagă ce poate să facă sistemul în cauză.
Interfețele sunt folosite pentru specificarea parametrilor unui model. În limbajul UML interfețele sunt clasificatoare și caracterizează numai unele părți de comportare a modelului. În diagrama cazurilor de utilizare interfețele reprezintă o entitate de operații, ce garantează un grup de servicii sau funcționalități pentru actori. Interfețele nu pot conține atribute, stări sau asociații cu direcții. Ele conțin numai operații fără indicarea realizării lor, formal interfețele sunt echivalente a unor clase abstracte ce definesc numai operații abstracte.
Următoarele cazuri de utilizare generale pot fi la îndemâna jucătorului:
Alegerea acțiunii dorite din Meniul Principal (MenuScreen);
Modificarea setărilor disponibile după preferința utilizatorului (SettingsScreen);
Afișarea statisticilor tuturor jocurilor de la instalarea aplicației și până în prezent (InformationsScreen);
Părăsirea aplicației urmată de mesaj tip dialog cu utilizator pentru corectitudinea deciziei (ExitScreen);
Începerea unui joc nou (GameScreen);
Punerea jocului în desfășurare în regim de pauză (PauseScreen);
Terminarea jocului în cazul în care obiectul controlat de utilizator a fost distrus de un anumit obstacol (EndGameScreen);
Actorii care interacționează cu sistemul sunt următorii:
jucătorul – acesta decide în orice moment al desfășurării aplicației unde și ce anume dorește să facă.
Precondiții: Aplicația trebuie să fie instalată și pornită.
Flux alternativ de evenimente ce pot surveni indiferent de poziția în aplicație:
întreruperea jocului de un eveniment (apel) și revenirea la starea actuală din joc atunci când apelul a luat sfârșit.
Părăsirea aplicației prin apăsarea butonului fizic Home (Acasă) al telefonului.
Diagrama generală a cazurilor de utilizare este prezentată în Figura 4.1 Cu ajutorul ei se poate delimita aria de cuprindere a sistemului și se pot identifica cerințele.
Figura 4.1 Diagrama generală a cazurilor de utilizare
4.2.1.1 Detalierea cazurilor de utilizare – Ecran Meniu
Meniul Principal: primul contact al utilizatorului cu jocul se îndeplinește aici, iar jucătorul poate opta ce acțiune dorește să îndeplinească (vezi Figura 4.2).
Operații posibile:
Modificarea setărilor disponibile după preferința utilizatorului (SettingsScreen);
Afișarea statisticilor tuturor jocurilor efectuate de jucător de la instalarea aplicației și până în prezent (InformationsScreen);
Începerea unui joc nou (GameScreen);
Părăsirea aplicației urmată de mesaj tip dialog cu utilizatorul pentru verificarea corectitudinii deciziei (ExitScreen).
Figura 4.2 Diagrama detaliată a cazurilor de utilizare – Meniu Principal
4.2.1.2 Detalierea cazurilor de utilizare – Ecran Setări
Meniu Setări: utilizatorul poate modifica anumite setări ale jocului dintre care amintesc pornire/ oprire melodii fundal, dar și reglare a volumului, pornire/ oprire efecte sonore și reglare a volumului, pornire/ oprire vibrații (vezi Figura 4.3).
Operații posibile:
jucătorul decide dacă pornește/ oprește melodiile de fundal după care reglează volumul în cazul în care hotărăște Pornirea Melodiilor de fundal;
jucătorul decide dacă pornește/ oprește efectele sonore după care reglează volumul în cazul în care hotărăște Pornirea Efectelor Sonore;
jucătorul decide dacă pornește/ oprește efectele tip vibrații în timpul jocului;
jucătorul poate salva setările cu scopul de a nu a mai fi nevoie să seteze de fiecare dată;
jucătorul poate reveni în meniul principal fără să salveze nimic.
Figura 4.3 Diagrama detaliată a cazurilor de utilizare – Meniu Setări
4.2.1.3 Detalierea cazurilor de utilizare – Ecran Statistici
Meniul Informatii/Statistici: afișarea statisticilor tuturor jocurilor efectuate de jucător de la instalarea aplicației și până în present (InformationsScreen). Deoarece este un meniu doar informativ jucătorul nu poate interveni și realiza anumite operații de alterare a datelor (vezi Figura4.4).
Operații:
jucătorul poate reseta statisticile;
jucătorul poate reveni în meniul principal după ce vizualizează informațiile.
Figura 4.4 Diagrama detaliată a cazurilor de utilizare – Meniu Statistici
4.2.1.4 Detalierea cazurilor de utilizare – Ecran Părăsire Joc
Meniul Părăsire joc: în cazul în care jucătorul optează pentru părăsirea jocului, acestuia îi va apărea o fereastră de dialog în care este întrebat dacă acesta este sigur de acest lucru (vezi Figura 4.5).
Operații:
jucătorul poate reveni în meniul principal;
jucătorul poate părăsi definitiv jocul.
Figura 4.5 Diagrama detaliată a cazurilor de utilizare – Meniu Părăsire Joc
4.2.1.5 Detalierea cazurilor de utilizare – Ecran Joc propriu-zis
Ecran Joc Propriu-Zis: odată ce jucătorul a decis începerea unui joc nou, aplicația se mută în ecranul propriu-zis al jocului și să încerce să se ferească de obstacole prin controlarea vagonului cu scopul de a rămâne cât mai mult în viață realizând astfel un scor cât mai mare (vezi Figura 4.6).
Operații:
Jocul va începe prezentând în prim-plan platforma și calea ferată de deasupra acesteia ce vor servi drept suport pentru mișcarea vagonului. Așadar, scopul jucătorului este de a controla mișcarea vagonului prin intermediul mișcării de tragere a inelelor (mânerelor) la capãtul cãrora este legat un cablu.
Jucătorul trebuie să se ferească de obiectele ce se rostoglesc pe grinzile din plan și ajung în final pe platformă având ca scop îngreunarea mișcării vagonului.
Jucătorul trebuie să adune elementele colectibile ce vor ajunge în cadrul de joc prin intermediul unor parașute ce au atașate diverse cutii cu comportament diferit.
jucătorul poate intra în meniul de pauză și în acest moment aplicația invocă un serviciu de salvare în memorie a stării curente a jocului astfel încât să se știe unde să revină după pauză.
Figura 4.6 Diagrama detaliată a cazurilor de utilizare – Ecran joc Propriu-zis
4.2.1.6 Detalierea cazurilor de utilizare – Ecran Pauză
Ecran Meniu Pauză: afișat doar atunci când jucătorul pune pauză în Ecranul jocului (vezi Figura 4.7).
Operații:
jucătorul poate revenii la joc, la starea curentă salvată în memorie;
jucătorul poate începe un joc nou;
jucătorul poate reveni în meniul principal.
Figura 4.7 Diagrama detaliată a cazurilor de utilizare – Ecran Pauză
4.2.1.7 Detalierea cazurilor de utilizare – Ecran Sfârșit de Joc
Ecran Sfârșit Joc: jucătorul ajunge aici doar atunci când jocul se termină (jucătorul rămâne fără vieți sau timpul de joc s-a epuizat) (vezi Figura 4.7).
Operații:
Jucătorul poate începe un joc nou;
Jucătorul poate reveni în Meniu Principal;
Oricare ar fi operația realizată, are loc salvarea statisticilor noi: de exemplu un scor nou record și afișarea acestuia în Meniul de Statistici/ Informații.
Figura 4.7 Diagrama detaliată a cazurilor de utilizare – Ecran Sfârșit de joc
4.2.2 Diagrama claselor
Locul principal în programarea orientată pe obiecte îi revine dezvoltării modelului logic al sistemului în diagrama claselor, al cãrei scop este de a structura natura statică a claselor.
Diagrama claselor servește pentru reprezentarea statică a modelului în terminologia claselor programării orientate pe obiecte. Diagrama claselor poate reflecta diferite relații între diferite esențe ale domeniului respectiv, ca obiecte, subsisteme, totodată determinând structura internă și tipurile de relații.
Diagrama claselor reprezintă un graf, vârfurile căruia sunt niște elemente de tip „clasificator”, care sunt într-o relație cu diferite tipuri structuri. De menționat că diagrama claselor deasemenea poate să conțină interfețe, pachete, relații și chiar exemplare aparte, ca obiecte și legături. Pe această diagramă sunt reprezentate structurile legăturilor modelului logic al sistemului, care la rândul său este static și nu depinde de timp.
Clasa în limbajul UML servește pentru notarea mulțimii de obiecte, ce au o structură, comportament și relații asemănătoare cu obiectele altor clase.
Numele clasei trebuie să fie unic în limitele proiectului și se recomandă să fie un substantiv având proprietățile apropiate acestei clasă.
Numele atributului prezintă o linie de text, ce se folosește în calitate de identificator atributului respectiv și de aceea ea trebuie să fie unică în limitele unei clase. În notația UML tipul atributului se determină conform limbajului de programare ales ca platformă pe care va fi construit sistemul.
În limbajul UML există o diferență între operație și metodă. Operațiile sunt acțiunile ce pot fi cerute de la orice obiect care face parte dintr-o clasă oarecare pentru schimbarea comportării; metoda este realizarea operației.
Diagrama claselor definește ierarhia claselor și moștenirea proprietăților lor. Spre exemplu clasa denumitã copil moștenește toate proprietățile clasei părinte, iar clasa moștenitoare are operațiile și metodele sale. O clasă părinte poate avea mai mulți moștenitori.
În figura 4.8. este reprezentată diagrama claselor care schițează în linii mari ecranele cele mai importante ale jocului. De exemplu toate ecranele (meniurile) au la bază o clasă abstractă pe care o extind cu scopul de a implementa comportamentul prorpiu care este specific fiecãrui meniu. Astfel în linii mari, toate meniurile trebuiesc inițializate, afișate, actualizate, ascunse, redimensionate și randate, numai că fiecare astfel de metodă este specifică fiecãrui ecran. Inițializarea ecranului de setări diferă de exemplu de cea din Ecranul de Joc. În această diagramă de clase am vrut sa evidențiez faptul că în meniul principal există câte o instanță a celorlalte ecrane pentru a le putea accesa și faptul că fiecare ecran are atribute și operații diferite.
Figura 4.8 Diagrama claselor celor mai importante ecrane din joc
4.2.3 Diagrama stărilor
Diagrama claselor reprezintă modelul logic al reprezentării statice a sistemului modelat. Pentru majoritatea sistemelor fizice în afară de reprezentarea statică este nevoie de o reprezentare dinamică. Diagrama stărilor este o reprezentare dinamică a sistemului proiectat.
Principalul scop al acestei diagrame este de a descrie secvența posibilă a stărilor prin care poate trece un exemplar a unei clase.
Ca atare diagrama dată este un graf ce prezintă un proces automat. Vârfurile acestui graf sunt stările acestui automat. Arcurile acestui graf reprezintă trecerea dintr-o stare a automatului în alta.
Automatele în limbajul UML reprezintă un formalism pentru modelarea comportamentului elementelor modelului și sistemului în general. În metamodelul limbajului UML automatul este un pachet, în care sunt specificate o mulțime de concepții necesare pentru modelarea comportamentului unei esențe în calitate de spațiu discret cu un număr finit de stări și treceri.
Principalele concepții a automatelor sunt starea și trecerea. Diferența dintre aceste două concepții estre că un obiect sau un exemplar poate să se afle într-o stare un timp nelimitat pe când trecerea are loc momentan. În caz general automatul reprezintă aspectele dinamice ale sistemului modelat.
Diagrama de stare are 3 feluri de stări:
început – este reprezentat în formă de cerc umplut de culoare neagră și corespunde stării obiectului în momentul creării;
desfășurare – reprezentat în formă de dreptunghi;
sfârșitul – se reprezintă în formă de cerc aflat în alt cerc.
Diagrama de stare poate avea numai un început, dar sfârșituri câte avem nevoie. Când obiectul se află într-o stare concretă asupra lui se pot îndeplini anumite procese.
Am ales să descriu în cele ce urmează diagrama Stărilor pentru Ecranul de joc (vezi Figura 4.9).
Figura 4.9 Diagrama Stărilor pentru Ecranul de joc
Odată ce jucătorul și-a exprimat dorința de a porni un joc nou, ecranul de joc este inițializat. Această inițializare constă defapt în instanțierea tuturor obiectelor necesare în joc cum ar fi:
– vagonul (obiectul a cărui poziție trebuie controlată),
– platforma (obiectul care trebuie controlat),
– mânerele (obiectele pe care utilizatorul le controlează direct),
– obstacolul (obiect de care jucatorul trebuie să se ferească deoarece acesta îi îngreunează mișcarea),
– elemente colectibile (parașute cu cadou ce activează un anumit comportament),
– scorul,
– viețile,
– timpul de joc
Dupã starea de inițializare, ecranul de joc pătrunde într-o buclă de stări. Pentru început este afișat totul în poziție inițială, după care se trece la starea următoare în care un obstacol alunecă pe grindă și pică pe platformă îngreunând vagonul, iar jucătorul trebuie să înceapă să preia elementele colectibile-parașută cu cadou, ecranul de joc aflându-se în stare de citire de noi valori de intrare de la jucător. Presupunând că jucătorul a tras de un mâner, modificând poziția lui și implicit a platformei, trebuie să aibă loc o împrospătare a atributelor tuturor elementelor implicate. Următoarea stare este de verificare a faptului că jucătorul nu a decis să pună jocul pe pauză. Dacă totuși l-a pus pe pauză trebuie afișat meniul de Pauză unde jucătorul poate opta să iasă din joc (Final) sau să revină la starea curentă salvată în memorie. Starea următoare a ecranului de joc este de verificare a coliziunilor elementelor pentru a determina calcularea scorului funcție de diverse bonusuri activate. Ciclul acesta se repetă atâta timp cât timpul de joc nu s-a epuizat și cât timp jucătorul mai are vieți disponibile.
4.2.4 Diagrama de secvență
Diagramele de secven ilustreaz interaciunile dintre obiecte sau dintre actori și obiecte din punct de vedere temporal. Un obiect este reprezentat printr-un dreptunghi i o bar vertical numit linia de via a obiectului. Mesajele sunt reprezentate prin sgei orizontale orientate de la emitorul mesajului ctre destinatar. Ordinea de trimitere este dat de poziia pe axa vertical. Timpul se scurge este orientat de sus n jos. Axa vertical poate fi gradat n scopul exprimrii mai exacte a constrngerilor temporale n cazul modelrii unui sistem de timp real.
Diagramele de secven se construiesc plecând de la cazurile de utilizare. Ele se pot folosi n dou scopuri, care corespund la douã nivele diferite ale procesului de dezvoltare:
Ca mijloc de documentare a cazurilor de utilizare; interaciunea este descris n termeni apropiai utilizatorului i fr a intra n detalii de sincronizare. Sgeile corespund evenimentelor care survin n domeniul aplicaiei.
Ca mijloc de reprezentare exact a mesajelor schimbate ntre obiecte. Perioada de activitate a unui obiect este reprezentat cu ajutorul unei benzi rectangulare suprapuse pe linia de via a obiectului.
Săgețile se folosesc pentru a reprezenta mesaje care corespund unui apel de procedură într-un flux de execuție cu un singur fir de execuție.
Trimiterea asincronă a unui mesaj nu ntrerupe execuia expeditorului (vezi Figura 4.10). Expeditorul trimite mesajul fr sã știe cnd, nici chiar dac mesajul va fi tratat de ctre destinatar. În figura urmãtoare este redată și confirmarea destinatarului după tratarea mesajului.
Figura 4.10 Exemplificare funcționare mesaj asincron
Jocul pornește, utilizatorul este întâmpinat cu un EcranInceput (SplashScreen), iar în fundal se încarcă toate resursele necesare jocului (sunete, imagini, font – se instanțiază anumite clase și se crează obiecte) după care se ajunge în Meniul Principal (MenuScreen) unde jucătorul optează pentru o anumită acțiune.
Flux principal de evenimente este urmãtorul (vezi Figura 4.11):
Apariție SplashScreen și încãrcarea tuturor resurselor de date necesare;
Încarcă în memorie toate imaginile;
Încarcă în memorie toate animațiile;
Încarcă în memorie toate melodiile;
Încarcă în memorie toate efectele sonore;
Încarcă fonturile de text folosite;
Încarcă preferințele salvate de utilizator;
Creează și pregătește un obiect al Clasei MenuScreen;
Pornire Meniu Principal și așteptare acțiune din partea utilizatorului.
Figura 4.11 Diagrama de secvență ce exemplifică încărcarea resurselor
Postcondiții: preferințele utilizatorului sunt stocate local în memoria telefonului, pe baza alegerilor făcute de acesta în prealabil în meniul de Setări. Dacă aplicația este rulată pentru prima dată și aceste setări nu există, ele vor fi inițializate cu anumite valori predefinite.
4.2.5 Diagrama de activitate
Diagramele de activitate se folosesc pentru modelarea aspectelor dinamice ale unui sistem, la diferite nivele. O diagramă de activitate poate reda pașii unui proces de calcul, fluxul controlului într-o operație și execuția secvențială sau paralelă a unor acțiuni.
O acțiune reprezintă un singur pas într-o activitate: un calcul, găsirea unor date, verificarea unor date, etc. O acțiune se reprezintă printr-un dreptunghi rotunjit în care este înscris text (numele acțiunii) în format liber.
Acțiunile redate într-o diagramă de activitate pot fi executate de diferite obiecte, care sunt active în același timp. Astfel, o diagramă de activitate poate reda, la un nivel de detaliu ridicat, interacțiunea dintre obiecte reprezentată printr-o diagramă de secvență.
Tipuri de noduri într-o diagramă de activitate:
Noduri executabile: noduri acțiune și noduri „tratare exceptie”;
Noduri de control: noduri de decizie, nod final (final activitate, final flux), nod Join, nod inițial, nod unificare (merge);
Noduri obiect.
Căi într-o diagramă de activitate:
Flux control
Flux obiect
Diagrama de activitate prezentată în Figura 4.12 ilustrează fluxul controlului într-o operație și execuția secvențială sau paralelă a unor acțiuni. Astfel, odată cu pornirea aplicației, fluxul în controlul acesteia este preluat de Ecranul de început care are rolul de a încarca în memorie toate elementele necesare (imagini, melodii, efecte sonore, animații, fonturi de text). Odată terminat acest proces, controlul este preluat de Meniul Principal care ramifică în activități concurente deciziile posibile ale jucătorului:
Meniu Setări unde poate regla dacă muzica este pornită sau oprită, activare vibrații, după care se poate întoarce în meniu principal;
Meniu Statistici unde jucătorul poate vizualiza anumite statistici după care se poate întoarce în meniu Principal;
Părăsire joc unde urmează o activitate de decizie din partea jucătorului, dacă este pozitivă atunci are loc finalul aplicației, dacă nu se întoarce în meniu principal;
Ecranul de Joc unde activitatea de bază constă în împrospătarea lumii jocului cu atributele actualizate ale elementelor după care are loc ramificarea în activități concurente de tipul: controlare vagon pe baza intrării de la jucător, tip calcul scor și tip decizie pentru a verifica dacă jucătorul nu mai are vieți sau timpul de joc s-a epuizat, iar în caz pozitiv se terminã jocul, în caz negativ se revine la împrospătarea jocului și se reiau activitățile.
Figura 4.12 Diagrama de activitate generală a jocului
Capitolul 5. Proiectare de detaliu și implementare
5.1 Instalarea și configurarea Motorului de Joc Libgdx
Primul pas în realizarea jocului este dat de crearea unui proiect nou în mediul integrat de dezvoltare utilizat Eclipse. De obicei, trebuiesc create mai multe proiecte în Eclipse: un proiect pentru codul de joc comun, un altul pentru echipamente tip desktop și multe altele pentru echipamente ce utilizează Android, iOS, și HTML5 / GWT. În plus, proiectele ar trebui, de asemenea, să fie configurate și legate între ele într-un anumit fel. Acest sarcină este destul de consumatoare de timp și mai mult sau mai puțin un proces ce generează erori pentru utilizatorii neexperimentați.
Din fericire, LibGDX oferă instrumente pentru generarea de proiecte preconfigurate pentru o nouă aplicație, care pot fi importate direct în Eclipse. Există două instrumente pentru crearea unui proiect LibGDX, cel mai recent folosește Gradle și va fi folosit în cele ce urmează.
Libgdx utilizează Gradle pentru gestionarea procesului de creare a aplicației. Gradle este un instrument de automatizare a procesului, open source, foarte asemănător cu Apache Ant și Apache Maven, care gestionează dependențele proiectului și descarcă biblioteci externe atunci când este necesar; dezvoltatorul de aplicații trebuind doar să declare că dorește să le includă în proiectul curent. Din fericire, framework-ul LibGDX oferă un instrument care creează scheletul aplicației cu toate elementele de bază necesare. Instrumentul gdx-setup oferă o interfață de utilizator foarte simplă, precum și o opțiune de linie-comandă [10]. Trebuiesc efectuați următorii pași:
În primul rând, trebuie descărcată cea mai recentă versiune a instrumentului precizat
de la adresa http://libgdx.badlogicgames.com/download.html (vezi Figura 5.1)
Figura 5.1 Descărcare instrument de configurare libgdx
După descărcare se rulează fișierul .jar ceea ce duce la deschiderea imediată a interfeței cu utilizatorul și se trece la completarea câmpurilor din Figura 5.2
Figura 5.2 Configurarea proiectului libgdx
Câmpul Name definește un nume comun pentru proiectul ce formează aplicația. Fiecare proiect lansator va adăuga propriul sufix la ea, cum ar fi -desktop, -android, sau -html.
Câmpul Package definește numele pachetului Java. Acest nume trebuie să fie un identificator unic, scris cu litere mici, care de obicei este derivat de la un nume de domeniu inversat. Nu trebuie să deținem un nume de domeniu și nici nu trebuie să existe într-adevăr, dar ajută în eliminarea eventualelor conflicte generate de nume pentru aplicații Java. Acest lucru este deosebit de important pe Android: astfel pentru nume de pachete identice pentru două aplicații diferite ar însemna că aplicația deja instalată va fi suprascrisă de a doua aplicație în timpul procesului de instalare.
Câmpul Gameclass definește numele clasei principale în proiectul de cod de joc comun.
Câmpul Destination definește folderul destinație unde vor fi generate toate proiectele.
Câmpul Android SDK definește calea către dosarul unde s-a optat pentru instalarea Android SDK.
Din secțiunea Sub Projects trebuiesc selectate platformele pentru care se intenționează dezvoltarea jocului și se poate alege dintre: Desktop, Android, Ios și Html. În cele din urmă, se selecteazã extensiile (de exemplu, Box2D, fizica tip glonț, și așa mai departe) care urmează să fie incluse în aplicație.
Din setările avansate am selectat mediul de dezvoltare integrat utilizat Eclipse.
Acum, că am stabilit totul, am dat click pe Generate. Va fi nevoie de un timp pentru a descărca și a genera noile proiecte. În cele din urmă, când totul este pregătit, se va afișa BUILD SUCCES ca în Figura 5.3:
Figura 5.3 Terminarea procesului de configurare a proiectului
Acest lucru înseamnă că sunt pregătit pentru a importa proiectul în IDE și pot începe să lucrez la realizarea jocului! Așadar am lansat în execuție mediul de dezvoltare integrat Eclipse și am trecut la importarea proiectelor generate în spațiul de lucru.
Pentru importarea proiectelor se navighează la opțiunea Import din meniul File. În Caseta de dialog denumită Import, se selectează subfolderul Gradle Project din folderul Gradle (ca în Figura 5.4).
Figura 5.4 Importarea proiectului Gradle în Eclipse
Acum, în fereastra de Import Gradle Project din Figura 5.5, se face click pe Browse și se selectează folderul definit în etapele precedente pentru crearea proiectului, apoi, se face click pe butonul Build Model. Va fi nevoie de un timp pentru a construi proiectul, iar după aceasta, se selectează proiectele pentru diferitele platforme dorite și se dă click pe Finish:
Figura 5.5 Importarea sub-proiectelor Gradle în Eclipse
Așadar, în final, graficul scheletului de dependențe al proiectui Libgdx va arăta ca în Figura 5.6:
Figura 5.6 Graficul scheletului de dependențe al proiectui [10]
5.1.1 Înțelegerea structurii proiectului și a ciclului de viață al aplicației
În acest subcapitol, voi examina arhitectura tipică a unui proiect Libgdx și modul în care aceasta face posibilă dezvoltarea cross-platform într-o manieră mult mai ușoară. Voi descrie cum trebuiesc configurate lansatoarele-platformă specifice, în scopul optimizării anumitor parametri, cum ar fi rezoluția, culorile, versiunea OpenGL și așa mai departe. Mai important, voi descrie cum decurge ciclul de viață al aplicației Libgdx.
Așa cum am menționat deja mai înainte, aplicații Libgdx sunt de obicei împărțite în mai multe proiecte: core, desktop, Android, iOS și HTML. Proiectele specifice platformelor servesc drept puncte de intrare ale aplicației pe fiecare platformă; datoria lor practic se reduce la invocarea clasei principale a proiectului de bază și trecerea parametrilor de configurare de bază pentru a putea rula jocul.
Deși prezentul proiect vizează exclusiv Android, puteam să mă rezum la producerea unui singur proiect care conține atât codul-platformă comun și Android-specific. Cu toate acestea, se dorește a fi de evitat o asemenea practică, deoarece în cazul portãrii jocului pe o platformă diferită nimeni nu ar dori să refacă structura proiectului în vederea adaptãrii la noile împrejurări. Indiferent de platformã și dispozitivele cu care se lucrează în prezent, este întotdeauna de preferat să se i-a în considerare și posibilele viitoare implementări pe diverse platforme.
Fiecare aplicație Libgdx are un ciclu de viață foarte bine definit, controlându-se cu precizie mare stările în care se poate afla aplicația la un moment dat. Aceste stări sunt: crearea, oprirea, reluarea, randarea și eliminarea. Ciclul de viață al aplicatiei este modelat de interfața ApplicationListener, care de altfel trebuie implementată în aplicație, deoarece va servi ca punct de pornire pentru logica jocului nostru.
Aceasta este definiția interfeței ApplicationListener:
public interface ApplicationListener {
public void create ();
public void resize (int width, int height);
public void render ();
public void pause ();
public void resume ();
public void dispose ();
}
Odată cu implementarea interfeței ApplicationListener se poate manipula fiecare dintre aceste evenimente în modul în care se consideră convenabil. Aici sunt descrise utilizările tipice:
create() : Această metodă este folosită pentru inițializarea subsistemelor și pentru încărcarea în memorie a resurselor.
resize() : Această metodă este folosită pentru stabilirea unei noi dimensiuni a ecranului; poate fi utilizată pentru repoziționarea elementelor UI (User Interface) sau pentru reconfigurarea obiectelor specifice camerei/ cadrului de joc.
render() : Această metodă este folosită pentru actualizarea și randarea elementelor din joc.
pause() : Această metodă este utilizată pentru a salva starea curentă a jocului atunci când se pierde prioritizarea jocului ca proces principal (intervenirea unui apel) sau când starea de joc se dorește a fi oprită intenționat de către jucător.
resume() : Această metodă este utilizată pentru a controla jocul atunci când se întoarce din pauză și asigură restabilirea stărilor jocului.
dispose() : Această metodă este folosită pentru eliberarea resurselor ce rezidă în memorie.
Figura 5.7 Ciclul de viață al aplicației[10]
Linia continuă și linia punctată conectează două stări consecutive și au definită ordinea întâmplării evenimentelor funcție de săgeata de la un capăt al liniei. O linie punctată reprezintă un eveniment specific sistemului.
Atunci când o aplicație pornește, va începe întotdeauna cu metoda create(). Aici este locul în care trebuie să se realizeze inițializarea aplicației, cum ar fi încărcarea în memorie a resurselor și crearea unei stări inițiale a lumii jocului. Ulterior, starea următoare este resize(). Acum aplicația are ocazia de a se adapta la dimensiunea de afișare disponibilă (lățimea și înălțimea) dată în pixeli.
Apoi, LibGDX se va ocupa de evenimentele specifice sistemului. Dacă nici un eveniment nu a avut loc între timp, se presupune că aplicația este (încă) în stare de funcționare fiind în vârful stivei de priorități. Următoarea metodă apelată va fi render() și acum o aplicație tip joc va face, în principal următoarele două lucruri: actualizarea elementelor lumii jocului și randarea pe ecran a acestor elemente.
Ulterior, o decizie se va face funcție de tipul de platformă utilizat care este detectat de LibGDX. Pe un desktop sau într-un browser web, fereastra aplicației poate fi redimensionată practic în orice moment. Astfel, LibGDX compară ultima și actuala dimensiune la fiecare ciclu, astfel încât metoda resize() va fi apelată numai dacă dimensiunea de afișare se schimbă. Acest lucru asigură că aplicația este capabilă să se acomodeze cu o nouă posibilă dimensiune de afișare.
Acum, ciclul se reia, prin controlarea de noi evenimente din sistem. Un alt eveniment din sistem care poate apărea în timpul rulării este evenimentul de ieșire. Atunci când apare, LibGDX va apela în primul rând metoda pause(), care este de altfel un loc foarte bun pentru a salva orice dată care s-ar putea pierde, după ce aplicația este închisă. Ulterior, LibGDX apelează metoda dispose() în care o aplicație ar trebui să facă o eliberare finală a resurselor ce rezidă în memorie.
Acest șir de evenimente este respectat cu mici diferențe pentru Android: excepția fiind dată de faptul că metoda pause() este o stare intermediară, care nu este urmată direct de metoda dispose() de prima dată. Trebuie ținut cont de faptul că acest eveniment ar putea avea loc în orice moment, în timpul rulării aplicației, deîndată ce utilizatorul a apăsat butonul Home sau dacă există un apel primit între timp. De fapt, atât timp cât sistemul de operare Android nu are nevoie de memoria ocupată de aplicația întreruptă, starea sa nu va fi schimbată spre apelarea metodei dispose(). Mai mult, este posibil ca o aplicație întreruptă să poată primi un eveniment sistem tip revenire, care, în acest caz, ar schimba starea aplicației spre apelarea metotei resume(), și ar ajunge în cele din urmă la sistemul de tratare a evenimentelor (system event handler) din nou.
5.1.2 Clasa Starter specifică platformei Android
O clasă starter definește punctul de intrare (punctul de plecare) al unei aplicații LibGDX. Codul scris este specific pentru o anumită platformă. De obicei, aceste tipuri de clase sunt foarte simple și constau în câteva linii de cod utilizate pentru a stabili anumiți parametri care se vor aplica platformei respecive. Aceste clase sunt asemănătoare unor secvențe de boot pentru fiecare platformă. Odată ce s-a realizat pornirea, LibGDX va muta controlul aplicației de la clasa starter cu codul specific platformei către clasele ce au codul comun prin apelarea diferitelor metode implementate.
Clasa starter pentru platforma Android se numește MainActivity.java. Pentru un proiect tip Gradle, clasa starter va fi AndroidLauncher.java.
Codul din Figura 5.8 face parte din AndroidLauncher.java
Figura 5.8 Clasa de start pentru Android
În lista de cod precedentă, se poate vedea clasa AndroidLauncher care este moștenită din clasa AndroidApplication. Acesta este modul în care LibGDX încapsulează sarcini, cum ar fi crearea unei așa-numite activități care înregistrează diverse procedee pentru procesarea intrării tip atingere, citirii datelor de la senzori etc. Ce rămâne de făcut, este crearea unei instanțe a unei clase care implementează interfata ApplicationListener. În acest caz, este o instanță a clasei EquilibriumMain. Instanțele obiectelor EquilibriumMain și AndroidApplicationConfiguration sunt transmise ca argumente metodei initialize(), metodă răspunzatoare de inițializare a sistemului.
În plus, pe Android, trebuie avut grijă de un fișier tip manifest care definește o listă foarte mare de parametri necesari configurării aplicației.
În Figura 5.9 este prezentat fișierul AndroidManifest.xml
Figura 5.9 Fișierul manifest al proiectului Android
Mai jos am explicat semnificația celor mai importante elemente:
minSdkVersion: Acesta este nivelul minim API necesar rulãrii aplicației. Dispozitivele ce rulează cu niveluri mai mici API, nu vor fi capabile sã ruleze această aplicație; dacă este nedeclarat, un Nivelul API 1 se va asigna în mod automat, dar asta ar putea duce la anumite incidente în timpul rulării aplicației.
targetSdkVersion: Acesta este nivelul API pentru care aplicația este destinată. Acest lucru este folosit pentru compatibilitate, astfel încât nivelurile de API mai recente să poată schimba comportamentul și înfățișarea API care ar putea bloca aplicațiile vechi. Această specificație nu împiedică aplicația să ruleze pe dispozitivele cu niveluri mai scăzute de API față de minSdkVersion. Dacă este nedeclarat, valoarea sa implicită este egală cu minSdkVersion.
icon: Aceasta este pictograma aplicației.
name: Aceasta este clasa principală a aplicație (sau activitatea principală). De menționat este faptul că, în ceea ce privește LibGDX, această clasă va fi clasă starter (de pornire) pentru Android.
label: Acesta este numele aplicației afișat lângă pictograma aplicației și în bara de titlu.
screenOrientation: Aceasta definește orientarea afișarii aplicației. Valorile uzuale sunt portret (portrait) și peisaj (landscape).
O altă parte esențială a fișierul manifest este definiția corectă a permisiunilor pe care aplicația ar trebui să le solicite, atunci când un utilizator dorește să o instaleze pe un dispozitiv. Este de preferat să nu se solicite permisiuni inutile, ci să se pună doar informația necesară în descrierea aplicației. Utilizatorii sunt extrem de suspicioși atunci când vine vorba de lista de permisiuni solicitate, deoarece nu este 100% clar motivul pentru care o aplicație are nevoie de o anumită permisiune.
Acum, că totul este pregătit, se poate încerca o rulare a aplicației pe un dispozitiv real pentru a observa dacă aceasta este compilată cu succes. Este de preferat să se utilizeze un dispozitiv fizic pentru testarea aplicației ci nu un emulator deoarece emulatoarele nu pot reflecta cu exactitate modul în care un dispozitiv răspunde, astfel încât este foarte recomandat ca aceste proceduri să se realizeze pe cât mai multe dispozitive reale.
5.2 Conceperea Jocului
5.2.1 Descrierea de bază a conceptului de joc
Privit de la distanță, un joc poate fi împărțit în două părți: game assets (Resursele Jocului) și game logic (Logica jocului).
game assets (Resursele Jocului) includ tot ceea ce va fi folosit ca și materiale de lucru în joc, cum ar fi: imagini, efecte sonore, muzică de fundal și date referitoare la nivel.
game logic (Logica jocului) este responsabilă pentru urmărirea stării curente de joc. Aceste stări se vor schimba foarte mult în timp din cauza evenimentelor declanșate fie de jucător fie de logica jocului în sine. De exemplu, atunci când un jucător apasă un buton, preia un articol/ element sau un inamic lovește jucătorul, logica jocului va decide acțiunile adecvate care trebuiesc efectuate. Toate acestea sunt cunoscute sub denumirea de gameplay și limitează modalitățile de acțiune în care un jucător poate interacționa cu lumea jocului și, de asemenea, modul în care lumea jocului va reacționa la acțiunile jucătorului.
Toate cele descrise mai sus se pot observa în Figura 5.10:
Figura 5.10 Descrierea conceptului de joc [11]
Primul pas este inițializarea jocului și constă în încărcarea în memorie a resurselor jocului, creînd starea inițială a acestuia, și înregistrarea subsistemelor utilizate, cum ar fi elementele specificice de intrare precum tastatura, mouse-ul ori atingere (touch), audio pentru redare și înregistrare, senzori și comunicarea în rețea.
Când totul este pregătit și jocul rulează, logica jocului este gata să preia controlul și se va crea o buclă infinită care va lua sfârșit doar la încheierea jocului. Acest tip de buclă infinită este denumită buclã de joc. În interiorul buclei de joc, logica jocului acumulează noile date necesare și pe baza lor actualizează modelul lumii jocului.
Este foarte important să se ia în considerare viteza cu care se vor actualiza datele în lumea jocului. În mod implicit, jocul va rula la viteza maximă de hardware disponibilă. În cele mai multe cazuri, acest lucru nu este un efect de dorit pentru că jocul va depinde în totalitate de puterea de procesare și de complexitatea șcenei care urmează să fie randată, care variazã de la dispozitiv la dispozitiv. Aceasta implică faptul că lumea jocului va progresa la viteze diferite pe diverse dispozitive, aceasta având un impact aproape întotdeauna negativ asupra gameplay-ul.
Cheia rezolvării acestei probleme este de a folosi delta times pentru a calcula progresul lumii jocului. Delta Time (Timpul delta) este timpul real dat de diferența dintre ultimul cadru redat și cadrul curent. Acum, fiecare actualizare a lumii jocului va avea loc în funcție de timpul real scurs de la ultimul cadru redat.
5.2.2 Descrierea jocului Equilibrium
La realizarea prezentului joc mi-am propus să mă ghidez dupã următoarea listă de idei referitoare la compoziția jocului plănuită încă de la începutul proiectului:
Numele jocului va fi Equilibrium;
Genul jocului: jocul va fi 2D având ca scop ținerea în echilibru a unui obiect principal;
Scopul jocului: controlarea unui obiect principal în vederea colectării unor elemente și obținerii unui scor cât mai mare în perioada de timp impusă;
Elementele jocului:
Obiectul principal – vagon, față de care jucătorul este interesat să îi controleze mișcarea și poziția astfel încât sã îndeplinească anumite sarcini;
Platforma și calea ferată de deasupra acesteia servesc ca platformã pentru mișcarea vagonului;
Mânerele (inelele) ce servesc la controlarea platformei, prin acțiuni de tragere pe verticală și orizontală;
Grinzile: elemente statice cu o înclinație bine definită utilizate pentru rostogolirea anumitor obiecte;
Scripeții: elemente prinse la capătul grinzilor ce sunt implicate în controlul mișcării platformei;
Elementele colectibile: sub formă de parașută cu un obiect atașat ce are o acțiune specifică;
Elementele ce îngreunează mișcarea vagonului: se rostogolesc pe grinzi și pică pe platformă;
Dealurile din fundal (utilizate pentru partea de decorațiuni a jocului);
Norii din fundal (utilizați pentru partea de decorațiuni a jocului);
Jocul va începe prezentând în prim-plan platforma și calea ferată de deasupra acesteia ce vor servi drept suport pentru mișcarea vagonului. Așadar, scopul jucătorului este de a controla mișcarea vagonului prin intermediul mișcării de tragere a inelelor (mânerelor) de care este legat un cablu la un capăt, cablu care trece printr-un scripete și este prins la celălalt capăt de platformã. Jucătorul trebuie să se ferească de obiectele ce se rostoglesc pe grinzi și ajung în final pe platformă având ca scop îngreunarea mișcării vagonului. Jucătorul trebuie să adune elementele colectibile ce vor ajunge în cadrul de joc prin intermediul unor parașute ce au atașate diverse cutii cu comportament diferit. Astfel, jucătorul trebuie să adune, în timpul de joc acordat, cât încă dispune de vieți, în principal toate cutiile cu diamante ce furnizează scorul jucătorului la un anumit moment dat, dar și scorul ce îl diferențiază în Clasamentul global. Cu diamantele colectate, jucătorul își poate îmbunătăți diverse caracteristici la vagon, la calea ferată, dar și anumiți timpi de utilizare a anumitor obiecte colectate.
Alte cutii ce pot fi colectate sunt de tipul:
Bombă: distrug vagonul și au ca rezultat pierderea unei vieți și reînceperea unui joc nou;
Magnet: atrage toate obiectele colectibile pe o anumită rază timp de câteva secunde;
Aspirator: atrage toate obiectele colectibile ce se află desupra vagonului timp de câteva secunde;
Invincibil: jucătorul nu va fi afectat de cutiile bombă timp de câteva secunde;
Viață în plus: jucătorul va primi o viață în plus;
Timp în plus: jucătorul va primi în plus câteva secunde de joc;
Scor dublu: toate cutiile cu diamante se vor puncta dublu timp de căteva secunde;
Jocul se termină atunci când perioada de joc s-a scurs ori când jucatorul și-a terminat toate viețile disponibile. O viață se poate pierde în doua moduri: fie jucatorul pică de pe platformă, fie jucătorul colectează un obiect tip bombă. Astfel, jucatorul trebuie să controleze cât mai bine înclinarea platformei, cu scopul de a oferi un echilibru justificat pentru o mișcare optimă a vagonului.
Pentru ca jocul să producă o provocare semnificativă, parașutele ce au cutiile atașate vor for fi generate la un anumit interval de timp bine definit, cu o șansă de generare specifică fiecărui obiect, în poziții întotdeauna aleatoare deoarece acestea au greutăți diferite având astfel și viteze de cădere diferite.
Așadar strângând mai multe diamante jucătorul va putea să își îmbunătățească caracteristicile vagonului, proprietățile căii ferate, dar și timpii în care un obiect colectat special va fi activat, toate acestea ducând la obținerea unui scor mai mare care va putea fi afișat și comparat cu scorurile altor jucători într-un clasament global.
În Figura 5.11 este prezentată o imagine de ansamblu a lumii jocului:
Figura 5.11 Vedere de ansamblu a lumii jocului Equilibrium
5.3 Etapele realizării jocului
În acest subcapitol se vor explica cele mai importante aspecte din etapa de realizare a jocului. Pentru început se vor analiza cele mai importante elemente ale codului demonstrativ care vine odată ce proiectul a fost configurat și realizat cu ajutorul programului utilitar oferit de LibGDX.
Analiza elementelor din următorul subcapitol este primordială deoarece acoperă cele mai importante concepte și noțiuni ce vor fi utilizate pe parcurs, sub diferite forme, în diferite ecrane ale jocului.
5.3.1 Analizarea codului demonstrativ al proiectului
Prima privire asupra codului se va realiza asupra clasei MyDemo.java din dosarul demo. Definiția clasei MyDemo.java:
public class MyDemo implements ApplicationListener {
// …
}
După cum se poate vedea clasa MyDemo implementează interfața ApplicationListener. Înainte de a trece la detaliile de implementare ale interfeței, voi acorda atenție și părții rămase a acestei clase. Se pot observa definițiile a patru variabile, fiecare având o clasă găzduită de motorul jocului Libgdx.
private OrthographicCamera camera;
private SpriteBatch batch;
private Texture texture;
private Sprite sprite;
Variabila camera este de tipul clasei OrthographicCamera. Voi folosi camera ortografică pentru afișarea șcenelor mele 2D. Camera reprezintă cadrul vizual văzut din perspectiva jucătorului asupra șcenei reale de joc, care este definită de o anumită lățime și înălțime (de asemenea, numită viewport).
Variabila batch este de tipul clasei SpriteBatch. Asupra acestei variabile îmi voi trimite toate comenzile de desenare. Dincolo de capacitatea acestei clase pentru a desena imagini, aceasta este capabilă și de optimizarea performanței de desenare, în anumite circumstanțe.
Variabila texture este de tipul clasei Texture și deține o referință la imaginea actuală; datele tip textură sunt stocate în memorie în timpul rulării.
Variabila sprite este de tipul clasei Sprite. Este un tip de date complex care conține o mulțime de atribute utilizate pentru reprezentarea unui obiect grafic definit de o poziție în spațiul 2D, lățime și înălțime. Dintre operțiile ce se pot efectua asupra obiectului grafic menționez rotirea și scalarea. Pe plan intern, acesta are o referință la o clasă TextureRegion care, la rândul ei specifică un mijloc de tăiere pentru o anumită parte a unei texturi.
Acum că am explicat scopul variabilelor și tipurilor de date implicate, pot avansa la detaliile de implementare ale interfeței ApplicationListener. În clasa MyDemo, singurele metode care dețin cod sunt create(), render() și dispose(). Celelalte trei metode rămase, discutate în capitolele precedente, sunt goale și pentru moment nu influențează cu nimic.
Metoda create() – Această metodă conține codul de inițializare al aplicației și este apelată la pornirea acesteia:
@Override
public void create() {
float w = Gdx.graphics.getWidth();
float h = Gdx.graphics.getHeight();
camera = new OrthographicCamera(1, h / w);
batch = new SpriteBatch();
texture = new Texture(Gdx.files.internal("data/libgdx.png"));
texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
TextureRegion region =new TextureRegion(texture, 0, 0, 512, 275);
sprite = new Sprite(region);
sprite.setSize(0.9f, 0.9f * sprite.getHeight() / sprite.getWidth());
sprite.setOrigin(sprite.getWidth() / 2, sprite.getHeight() / 2);
sprite.setPosition(-sprite.getWidth() / 2,-sprite.getHeight() / 2);
}
La început, modulul grafic este apelat spre a întoarce lățimea și înălțimea de afișare (de exemplu, o fereastră pe desktop sau ecranul unui dispozitiv Android) și apoi se calculează o lățime și înălțime corespunzătoare pentru cadrul de vizualizare al camerei. Apoi, o nouă instanță SpriteBatch este creată astfel încât imaginile pot fi desenate și făcute vizibile la camera. Următorul pas este de a încărca o textură, folosind modulul de fișiere, pentru a obține un manager de fișiere cãtre data/libgdx.png.
Textura încărcata arată ca în Figura 5.11:
Figura 5.11 Rezultatul rulării codului demonstrativ
După cum se poate vedea, există o mulțime de spațiu gol în această imagine. Cu scopul de a putea folosi doar partea umplută din imagine, este creată o nouă instanță de TextureRegion. Acesta are o referință către textura încărcată anterior, care conține imaginea completă și care are informațiile suplimentare, pentru a putea tăia din imagine, începând de la (0, 0) la (512, 275). Aceste două puncte descriu un dreptunghi pornind de la colțul din stânga sus al imaginii, cu o lățime și înălțime de 512, respectiv 275 pixeli. În cele din urmă, o instanță sprite este creată folosind informațiile din regiunea texturii creată anterior. Dimensiunea sprite-ului este setată la 90% din dimensiunea originală. Originea sprite-ului este setată la jumătate din lățime și înălțime pentru a se muta originea în centrul său. În cele din urmă, poziția este setată la jumătatea negativă din lățimea și înălțimea sprite-ului, astfel încât sprite-ul se va deplasa în centrul șcenei.
Libgdx utilizează un sistem de coordonate care își are originea (0, 0) în colțul din stânga-jos a ecranului. Acest lucru înseamnă că axa x are puncte pozitive înspre dreapta, iar axa y are puncte pozitive în sus.
Metoda render() – Această metodă conține comenzi ce fac posibilă o randare a șcenei pe ecran:
@Override
public void render() {
Gdx.gl.glClearColor(1, 1, 1, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
batch.setProjectionMatrix(camera.combined);
batch.begin();
sprite.draw(batch);
batch.end();
}
Primele două linii apelează metodele OpenGL de nivel jos necesare pentru a seta culoarea tip solid alb și pentru executarea comenzii de eliberare a ecranul.
Apoi, matricea de proiecție pentru sprite batch este setată la matricea combinată de proiecție și vedere a camerei. Aceasta înseamnă practic că fiecare comandă de desen următoare se va alinia la regulile unei proiecții ortografice, sau mai simplu spus, desenul cu pricina se va face în spațiu 2D utilizând poziția și limitele camerei.
Metodele begin() și end() trebuiesc întotdeauna să apară în pereche și nu trebuie să fie imbricate niciodată, altfel vor apărea erori. Desenul efectiv a sprite-ului se realizează prin apelarea metodei draw() a instanței sprite, trimițând totodată ca parametru și instanța obiectului sprite batch.
Metoda dispose() – Acesta este locul unde ar trebui eliminate toate resursele care sunt încă în uz, la acel moment dat de către aplicație:
@Override
public void dispose() {
batch.dispose();
texture.dispose();
}
Există o interfață numită Disposable, care este implementată automat în orice aplicație de către fiecare clasă tip Libgdx, care alocă resurse (în memorie) și care pot fi ușor de-alocate prin apelarea metodei dispose() corespunzătoare. În codul precedent, acest lucru se face pentru instanțele batch Sprite și texture încărcate în memorie.
5.3.2 Realizarea propriu-zisă a jocului
5.3.2.1 Realizarea scheletului de clase de bază al jocului
Acum după ce s-a făcut o trecere în revistă a elementelor de bază ale unei aplicații tip LibGDX sunt pregătit pentru a începe dezvoltarea unui joc real. Deoarece Libgdx este un framework, ci nu un motor de joc propriu-zis, trebuie mai întâi să ne definim propriul nostru motor de joc asociat framework-ului LibGDX. Deci, în cele ce urmează voi explica cum se poate crea o arhitectură de program adecvată necesară pentru a gestiona jocul meu.
Încă de la început am să îmi definesc următoarele clase:
Clasa WorldController conține toată logica de joc utilizată pentru a inițializa și modifica elementele ce țin de lumea jocului. Este nevoie, de asemenea, să ofere acces la clasa CameraHelper, ce este o clasă de ajutor pentru camera cadru, care, de exemplu, îi permite să vizeze și să urmarească un anumit obiect din joc, în cazul meu vom observa că este cazul vagonului, obiect pe care camera va fi focusat. Clasa WorldController va avea o instanță a clasei WorldLevel care deține datele de nivel; și o listă de obiecte AbstractGameObject ce reprezintă orice obiect care există în lumea jocului.
Randarea are loc în clasa WorldRenderer ce necesită, de asemenea, să aibă acces la lista de obiect tip AbstractGameObject. Deoarece obiectele din joc trebuie să fie create înainte de procesul de modificare și randare, WorldLevel are nevoie de acces la lista de obiecte AbstractGameObject.
De menționat este faptul că toate elementele mele tip obiect ce aparțin lumii jocului, reprezintă o clasă specializată ce extind clasa abstractă AbstractGameObject, deoarece toate împărtășesc o funcționalitate comună și anume de a fi un obiect general în lumea jocului care poate fi randată în cadrul de joc.
Mi-am definit o clasă de utilități pentru stocarea valorilor constante și am denumit-o Constants și aceasta are ca scop evitarea împrăștierii sau, chiar mai rău, duplicarea anumitor constante prin toate fișierele de cod sursă. Așadar, valorile stocate în Constants sunt destinate a fi utilizate în orice altă clasă.
O parte din clasă Constants:
package ro.blutec.equilibrium.util;
public class Constants {
// Visible game world is 10 meters wide
public static final float VIEWPORT_WIDTH = 10.0f;
// Visible game world is 10 meters tall
public static final float VIEWPORT_HEIGHT = 10.0f;
}
În primul rând, am avut nevoie să definesc dimensiunea pentru lumea vizibilă, care poate fi văzută de-odată, atunci când se realizează o deplasare în lumea jocului. În acest caz, am ales o dimensiune vizibilă de zece metrii în ceea ce privește lățimea și înălțimea.
În continuare, voi crea cele trei clase menționate, dar numai adăugând așa-numitele metode goale (doar definițiile lor). În acest fel voi descrie scheletul la început, după care voi adăuga cod și alte caracteristici noi mai târziu pentru a putea oferi o abordare pas cu pas în procesul de dezvoltare, în ansamblu, de la început până la sfârșit.
Implementarea clasei EquilibriumMain
import com.badlogic.gdx.ApplicationListener;
import ro.blutec.equilibrium.game.WorldController;
import ro.blutec.equilibrium.game.WorldRenderer;
public class EquilibriumMain implements ApplicationListener {
private static final String TAG = EquilibriumMain.class.getName();
private WorldController worldController;
private WorldRenderer worldRenderer;
@Override public void create () { }
@Override public void render () { }
@Override public void resize (int width, int height) { }
@Override public void pause () { }
@Override public void resume () { }
@Override public void dispose () { }
}
Această clasă implementează ApplicationListener pentru a deveni o clasă de pornire specifică LibGDX.
O referință către WorldController și WorldRenderer permit aceastei clase să actualizeze și controleze fluxul de joc ș, de asemenea, să facă posibilă o randare pentru starea actuală a jocului la ecran.
Există o variabilă TAG care păstrează o etichetă unică ce derivează din numele clasei. Acesta va fi utilizat pentru scopuri de vizualizare specifice unui fișier jurnal. LibGDX oferă un sistem de realizare a unui jurnal al evenimentelor care va cere să trec și un nume de etichetă pentru fiecare mesaj recepționat. Deci, pentru a rămâne consecvent în codul meu, voi adăuga pur și simplu o variabilă etichetă pentru fiecare clasă.
Implementarea clasei WorldController
package ro.blutec.equilibrium.game;
public class WorldController {
private static final String TAG =WorldController.class.getName();
public WorldController () { }
private void init () { }
public void update (float deltaTime) { }
}
Această clasă are o metodă init() utilizată la inițializare. Desigur, tot codul de inițializare poate fi pus în constructorul clasei. Cu toate acestea, se pare a fi mai util, atunci când un cod de inițializare este disponibil într-o metodă separată. Ori de câte ori am nevoie să resetez un obiect în joc, se întamplă să nu vreau întotdeauna ori să trebuiască să-l reconstruiesc complet, economisind astfel o mulțime de performanță. De asemenea, această abordare poate reduce foarte mult întreruperi ale Garbage Colector (GC). În schimb, voi încerca să reutilizez obiecte existente, fapt ce este de altfel întotdeauna și un scop de design recomandat, folositor pentru a maximiza performanțele și minimiza utilizarea memoriei. Acest lucru este valabil mai ales pentru smartphone-uri, cum ar fi Android cu resurse limitate.
Metoda update() va conține logica de joc și va fi apelată de sute de ori pe secundă. Este nevoie de un timp delta, astfel încât să poată aplica actualizările în lumea jocului în funcție de fracțiunea de timp care a trecut de la ultimul cadru redat.
Configurațiile pentru clasele starter utilizează sincronizarea verticală (VSYNC), care este activată în mod implicit. Folosind VSYNC va limita rata de împrospătare a cadrelor și de asemenea apelurile către metoda update() la un maxim de 60 de cadre pe secundă.
Implementarea clasei WorldRenderer
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.Disposable;
import ro.blutec.equilibrium.util.Constants;
public class WorldRenderer implements Disposable {
private OrthographicCamera camera;
private SpriteBatch batch;
private WorldController worldController;
public WorldRenderer (WorldController worldController) { }
private void init () { }
public void render () { }
public void resize (int width, int height) { }
@Override public void dispose () { }
}
Această clasă are, o metodă internă init() pentru inițializarea acesteia. În plus, conține o metodă render() care dispune de o logicã necesară pentru a defini ordinea în care obiectele din joc sunt randate. Ori de câte ori dimensiunea ecranului se schimbă, inclusiv în cazul evenimentului de la începutul programului, resize() va intra în acțiune inițializând măsurile necesare pentru a se potrivi cu noua situație.
Randarea este realizată folosind o camerã ortografică, care este potrivită pentru proiecții bidimensionale, necesare în cazuri precum jocul meu. Din fericire, LibGDX vine cu o clasă OrthographicCamera pentru a simplifica sarcinile de randare 2D. Clasa SpriteBatch reprezintă un punct esențial care desenează toate obiectele noastre, ținând totodată cont de setările curente ale camerei (de exemplu, poziția, zoom și așa mai departe) pe ecran. Deoarece SpriteBatch implementează interfața Disposable specifică LibGDX, este recomandabil să se apeleze mereu metoda dispose() pentru a elibera memoria alocată când nu mai este necesară. Voi face acest lucru și în WorldRenderer prin implementarea interfeței Disposable. Acest lucru ne permite să accesăm cu ușurință procesul de eliminare în cascadă, când se apelează metoda dispose() din EquilibriumMain. În acest caz, voi apela simplu metoda dispose() din clasa WorldRenderer, care, la rândul său, va apela metoda dispose() a clasei SpriteBatch.
Se poate observa că această clasă necesită o referință la o instanță WorldController în constructor, astfel încât aceasta va fi accesibilă mai târziu, pentru a putea face ca toate obiectele ce aparțin lumii jocului (controlate de WorldController) să poată fi randate.
5.3.2.2 Realizarea buclei de joc
Bucla de joc se va afla în metoda render() din clasa EquilibriumMain. Înainte de a putea adăuga noul cod, trebuie să import următoarele pachete pentru a avea acces la anumite clase pe care le voi folosi:
import com.badlogic.gdx.Application;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
Astfel dupã importarea claselor necesare am adăugat următorul cod în metoda create():
@Override
public void create () {
// Set Libgdx log level to DEBUG
Gdx.app.setLogLevel(Application.LOG_DEBUG);
// Initialize controller and renderer
worldController = new WorldController();
worldRenderer = new WorldRenderer(worldController);
// Game world is active on start
paused = false;
}
În primul rând, am stabilit nivelul jurnal al sistemului implicit LibGDX pentru a se putea depana și imprima totul în consolă în timpul rulării aplicației. Înainte de publicarea jocului, nivelul de jurnal trebuie resetat la ceva mai potrivit, cum ar fi LOG_NONE sau LOG_INFO.
După aceasta, pur și simplu am creat o nouă instanță de WorldController și WorldRenderer și le-am salvat în variabilele membre respective.
Așa cum am vazut în capitolele precedente, există evenimente de sistem specifice Android, pentru întreruperea și reluarea aplicațiilor. În cazul unei eveniment tip pauză sau reluare a stării jocului, ar fi de preferat ca jocul să se oprească sau să continue actualizarea lumii de joc. Pentru a face acest lucru posibil, am declarat o nouă variabilă numită paused.
Pentru realizarea efectivă a buclei de joc am scris următorul cod în metoda render():
@Override
public void render() {
// Do not update game world when paused.
if (!paused) {
// Update game world by the time that has passed since last rendered frame.
worldController.update(Gdx.graphics.getDeltaTime());
}
// Sets the clear screen color to: Blue
Gdx.gl.glClearColor(0x64/255.0f, 0x95/255.0f, 0xed/255.0f,0xff/255.0f);
// Clears the screen
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// Render game world to screen
worldRenderer.render();
}
Lumea jocului este actualizată folosind timpul delta. Din fericire, LibGDX realizează matematica și toate cele necesare aflate în spatele acestui timp delta pentru noi, așa că tot ce trebuie să fac este să interoghez valoarea apelând getDeltaTime() din modulul Gdx.graphics și să îl transmit ca și parametru pentru metoda update() din WorldController. După aceasta, se execută două apeluri OpenGL directe folosind modulul Gdx.gl. Primul apel glClearColor() stabilește culoarea albastru deschis, folosind valori roșu, verde, albastru și alpha (RGBA) scrise în notație hexazecimală. Fiecare componentă de culoare trebuie să fie exprimată ca o valoare în virgulă mobilă cuprinsă între 0 și 1, cu o rezoluție de 8 biți. Acesta este motivul pentru care se realizează și divizarea pentru fiecare componentă de culoare cu valoarea 255.0f (8 biți = 28 = 256 = 0..255 niveluri distincte pe componenta de culoare).
Cel de-al doilea apel glClear() folosește culoarea stabilită înainte pentru a umple ecranul și prin urmare, șterge tot conținutul ecranului precedent. Ultimul pas face randarea noului cadru al lumii jocului pe ecran.
Nu ar trebui să se inverseze ordinea de executarea a codului din lista precedentă. De exemplu, se poate încerca mai întâi să se randeze și apoi să actualizeze lumea jocului. În acest caz, lumea jocului va rămâne întotdeauna cu un cadru afișat în spatele stării sale actuale. Schimbarea este foarte subtilă și ar putea trece chiar neobservată. Aceasta, desigur, depinde de mulți factori: dacă este un joc de acțiune ce necesită reacții rapide, efectul va fi mult mai vizibil în comparație cu un joc lent cu suficiente pauze pentru a reduce decalajul de timp până când ecranul afișează în cele din urmă adevărata stare din lumea de joc.
După aceasta am adăugat următorul cod în metoda resize():
@Override
public void resize (int width, int height) {
worldRenderer.resize(width, height);
}
Ori de câte ori are loc un eveniment de redimensionare, metoda resize() a interfeței ApplicationListener se va apela. Deoarece acest eveniment are legătură cu randarea efectivă, vreau ca schimbările să se reflecte și în WorldRenderer și prin urmare, pur și simplu transmit valorile de intrare noi pentru metoda proprie de redimensionare resize().
Același lucru este valabil și pentru codul adăugat în dispose():
@Override
public void dispose() {
worldRenderer.dispose();
}
Ori de câte ori are loc un eveniment de eliberare a resurselor, este trecut mai departe la instanța clasei specializată în randarea obiectelor.
În cele din urmă, am scris următorul cod pentru a întrerupe jocul în metoda pause() și pentru a relua starea jocului în metoda resume():
@Override
public void pause () {paused = true;}
@Override
public void resume () {paused = false;
}
Înn Figura 5.13 este simbolizată toatã muncă descrisă în aceste subcapitole:
Figura 5.13 Rezultatul realizării unei simple bucle de joc
Rezultatul muncii de până acum este un ecran umplut în întregime cu o culoare albastră. Deși rezultatul nu este foarte interesant încă și nici nu seamănă cu nimic ce duce cu gândul la un joc, toată munca făcută reprezintă fundamentul pe care pot continua să construiesc următoarele extensii pentru joc.
5.3.2.3 Administrarea resurselor jocului
În acest subcapitol mă voi focusa pe administrarea resurselor vizuale ale jocului. În acest scop îmi voi realiza o clasã Assets specializată în controlul resurselor vizuale.
Înainte de a începe crearea unei texture atlas, doresc să explic mai întâi de ce această tehnică este benefică. O textură atlas este o imagine obișnuită, care poate fi redată pe ecran la fel ca orice altă imagine. Aceasta este utilizată ca și imagine container care are mai multe subimagini mici aranjate în așa fel încât să nu se suprapună între ele și să se încadreaze în dimensiunile fișierului atlas. În acest fel, putem reduce foarte mult cantitatea de texturi care sunt trimise la procesorul grafic, ceea ce va îmbunătăți în mod semnificativ performanța. Texturile atlas sunt utile mai ales pentru jocuri în care există o mulțime de imagini mici și diferite ce sunt randate în același timp. Motivul esențial al implementării este dat de comutarea între diferite texturi ceea ce reprezintã un proces foarte costisitor din punct de vedere al timpului. De fiecare dată când se schimbă texturile în timpul randării, noi date trebuie să fie trimise în memoria video. Acest lucru poate fi evitat dacă utilizăm aceeași textură pentru tot.
Texturile atlas nu numai că vor crește rata de împrospătare a cadrelor jocului în mod semnificativ, dar vor permite, de asemenea, folosirea subimaginilor ce nu sunt de tip texturi puterea lui doi. Motivul subimaginlor noastre este că pot să aibă o dimensiune arbitrară deoarece regula la puterea a doua se aplică numai texturilor care sunt încărcate în memoria video. Prin urmare, atunci când randăm o sub-imagine utilizez textura atlas, care este o textură de putere a lui doi ca dimensine a pixelilor.
În Figura 5.14 este prezentatã textura atlas utilizată de mine:
Figura 5.14 Fișierul atlas al jocului Equilibrium
Mai departe, pentru încărcarea texturii atlas în jocul meu am utilizat o clasã AssetManager, iar pentru organizarea elementelor în cadrul jocului mi-am definit o clasă Assets ce are metode definite pentru realizarea legăturii dintre o subimagine și obiectul meu din joc. Această clasã are o caracteristică specială dată de utilizarea modelului de design numit Singleton ce asigură existența unei singure instanțe Assets. Acest lucru are mult sens aici, deoarece nu există nici un motiv de a avea mai multe instanțe care pointează spre aceleași resurse. Un Singleton este implementat prin definirea unui constructor privat care previne astfel instanțierea. Va exista o variabilă internă constantă instance care pointează către instanța curentă a clasei permițând doar citirea și accesarea virtuală de oriunde din jocul meu.
5.3.2.4 Realizarea scenei de joc
În acest subcapitol, voi realiza o scenă care prezintă lumea actuală de joc. Lumea jocului va fi compusă din mai multe obiecte de joc care împărtășesc atribute și funcționalități comune. După implementarea obiectelor de joc și a inițiatorului lumii, voi pune noul cod în acțiune prin adăugarea lui la WorldController și WorldRenderer, clasele ce definesc lumea jocului meu.
În Figura 5.15 este ilustrată șcena mea de joc:
Figura 5.15 Șcena de joc a lumii jocului Equilibrium
Înainte de a începe implementarea fiecărui obiect al jocului în parte, am creat o clasă abstractă numită AbstractGameObject. Aceasta va deține toate atributele și funcționalitățile comune pe care fiecare obiect din joc le va moșteni.
Această clasă este capabilă de a stoca poziția, dimensiunea, originea, factorul de scalare și unghiul de rotație al unui obiect de joc. Metodele sale update() și render(), vor fi apelate din interiorul controler-ului lumii noastre, respectiv din mecanismul de redare în consecință.
Așadar, am început să îmi construiesc pe rând clase specifice pentru obiectele mele din joc cum ar fi: vagon, platformă, cale ferata, grindă, parașută cu cadou, frânghii etc., toate extinzând clasa AbstractGameObject și moștenind anumiți parametrii ce definesc comportamentul, iar alții find suprascriși cu un comportament specific al obiectului.
Din comportamentul specific al unui obiect, doresc să remarc modul de construcție al obiectelor tip cale ferată sau grindă. Acestea se deosebesc de restul obiectelor deoarece modul de compoziție este format din multiplicarea unor texturi pe întreaga lungime a obiectului. Toate sub-texturile componente sunt aranjate în containerul obiect astfel încât să nu se suprapună, ținând cont de unghiul de înclinație; toate acestea fiind posibile prin dezvoltarea unor algoritmi complecși ce au la bază utilizarea principiilor trigonometriei.
O atenție deosebită trebuie avută însă la randarea obiectelor din lumea jocului la cadrul camerei, deoarece acest eveniment trebuie să țină cont ce elemente care trebuies să facă parte din fundal, implicit elementele care trebuie desenate primele, pentru ca obiectele desente după ele să le poată suprapună. Ca o comparație la acest eveniment se poate considera ordinea desenării layer-elor (stratelor).
Lumea jocului va fi asemeni unei simulări a fizicii și va permite oricărui obiect din joc să fie mișcat folosind proprietăți fizice, cum ar fi viteza, accelerația și frecarea. În plus, logica jocului va trebui să fie capabilă să detecteze coliziuni ale obiectelor din joc în vederea declanșãrii unor anumite evenimente. De exemplu, vrem ca atunci când vagonul prinde o parașută cu cadou (are loc o coliziune a obiectelor) să se activeze un anumit comportament specific obiectelor implicate prin acționarea unui eveniment predefinit cum ar fi invincibiltatea la coliziunea cu o bombă pentru un timp stabilit, ori dublarea scorului, ori aspirarea tuturor parașutelor cu cadou de deasupra vagonului etc. Logica jocului va include, de asemenea un control pentru a afla dacă jocul s-a sfârsit, prin pierderea tuturor vieților disponibile ori epuizarea timpului de joc, astfel încăt jocul să se termine imediat cu afișarea unui mesaj de tip text corespunzător.
Următoarea listă conține o scurtă descriere a proprietăților fizice pe care le posedă obiectele din joc:
Velocity(Viteza): Aceasta este viteza obiectului curent mãsuratã în m/ s.
TerminalVelocity (Viteza maximă): Aceasta este viteza maximă pozitivă și negativă a obiectului mãsuratã în m/ s.
Friction (frecare): Aceasta este o forță opusă, care încetinește obiectul până viteza acestuia devine egală cu zero. Această valoare este dată ca un coeficient și este adimensională. O valoare zero înseamnă fără frecare și astfel viteza obiectului nu va scădea.
Acceleration (accelerație): Aceasta este accelerația constantă a obiectului mãsuratã în m/ s².
Bounds (limite): „casetă de încadrare” a obiectului ce definește corpul fizic care va fi utilizat pentru detectarea coliziunilor cu alte obiecte. Caseta de încadrare poate fi setată la orice dimensiune și este complet independentă de dimensiunea reală a obiectului în lumea jocului.
În sprijinul comportării cât mai fidele, din punct de vedere al fizicii obiectelor, am implementat librăria Box2D-un motor de fizică pentru a simula cât mai realist fizica obiectelor în spațiu 2D.
Box2D este un motor de fizică open source care are scopul de a simula corpuri rigide în spațiul 2D. Codul independent de platformã este scris în C++ încurajându-se astfel portarea în mai multe motoare de joc și limbaje de programare în general. Exemple de jocuri populare unde se poate vedea libraria Box2D în acțiune: Angry Birds, Limbo, Tiny Wings, și Crayon Physics Deluxe.
LibGDX integrează Box2D, care este similar cu alte framework-uri, printr-un API propriu asemănător cu API original Box2D. Această abordare face ușoară transferarea cunoștințelor existente despre Box2D regăsite în manualul oficial Box2D.
Mai întâi de toate, trebuie clarificat ce înseamnă termenul de corp rigid. Un corp, în sensul fizicii, este doar o colecție de materie cu unele atribute care îi sunt încredințate, precum poziția și orientarea. Este ceea ce numim de obicei un obiect în lumea noastră reală. Acum, un corp așa-numit rigid descrie un corp idealizat care se consideră a fi solid și deci imposibil de deformat de către forțele exercitate.
Pe lângă atributele de poziție și orientare în 2D, un corp are, următoarele caracteristici:
masă dată în kilograme
viteză (viteză direcționată) mãsuratã în metri pe secundă (m/ s)
viteza unghiulară (viteza de rotație) dată în radiani pe secundă (rad/ s).
Există trei tipuri diferite de corpuri pe care le-am implementat și anume:
Static: Acesta este un corp staționar ce nu se ciocnește cu alte corpuri statice sau cinematice. Este util pentru teren, pereți, platforme non-mișcare și așa mai departe. În jocul meu corpurile statice sunt grinzile din partea stăngâ și dreaptă.
Cinematic: Acesta este un corp mobil. Poziția poate fi actualizată sau modificată manual în funcție de viteza sa, ce reprezintă o metodă preferată și fiabilă. Corpurile cinematice nu se ciocnesc cu alte corpuri statice sau cinematice. Ele sunt utile pentru platforme de deplasare (de exemplu, lifturi) și așa mai departe. În jocul meu corpurile cinematice sunt mânerele față de care sunt prinse frânghiile de control a platformei, scripeții din capătul grinzilor, câmpul magnetic, câmpul tip aspirator.
Dinamic: Acesta este un corp mobil. Poziția poate fi actualizată sau modificată manual, în funcție de forțe. Corpurile dinamice se pot ciocni cu toate tipurile de corpuri descrise. Acestea sunt utile pentru jucători, inamici, obiecte și așa mai departe. În jocul meu corpurile dinamice sunt platforma pe care se află calea ferată care este de asemeni un corp dinamic, vagonul, parașuta cu cadou, bolovanii care se rostogolesc pe grinzi.
Un shape (o formă) descrie corpurile 2D într-un mod geometric folosind raze de cercuri, lățimi și lungimi de dreptunghiuri sau un anumit număr de puncte (noduri) pentru forme mai complexe, folosind poligoane. Deci, aceste forme definesc zonele care pot fi testate pentru coliziuni cu alte forme. Dintre formele utilizate pot menționa ca cercuri corpurile tip roți, scripeții; forme dreptunghiulare corpurile tip grinzi, platforma, iar ca formă poligon corpul de tip vagon.
Fixture (o prindere) folosește exact o formă la care se adaugă proprietățile de material, cum ar fi densitatea, frecarea și elasticitatea. Forma (shape) definită într-o prindere (fixture) este apoi atașată unui corp prin adăugarea acestei fixture. Deci fixture (prinderea) joacă un rol important în modul în care corpurile interacționează unele cu altele.
World (Lumea) este spațiul virtual în interiorul căruia are loc simularea fizică. Fiecare corp trebuie să fie inserat în lume pentru a fi inclus în simulare.
Exemplu de implementare a fizicii jocului (este descrisă realizarea câmpului tip aspirator):
public World b2world;
private void initPhysics () {
if (b2world != null) b2world.dispose();
b2world = new World(new Vector2(0, -9.81f), true);
Vector2 origin = new Vector2();
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyType.KinematicBody;
bodyDef.position.set(vacuum.position);
Body body = b2world.createBody(bodyDef);
vacuum.body = body;
PolygonShape polygonShape = new PolygonShape();
origin.x = vacuum.bounds.width / 2.0f;
origin.y = vacuum.bounds.height / 2.0f;
polygonShape.setAsBox(vacuum.bounds.width / 2.0f,vacuum.bounds.height / 2.0f, origin, 0);
FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = polygonShape;
body.createFixture(fixtureDef);
polygonShape.dispose();
}
Clasa World reprezintă lumea de care am vorbit puțin mai sus. Așadar, voi crea o nouă instanță a clasei World și o voi păstra în variabila b2world pentru o consultare ulterioară. Constructorul clasei World necesită o instanță de Vector2 pentru simularea gravitației lumii și un al doilea parametru care controlează inactivitatea corpurilor. De obicei, acest indicator ar trebui să fie activat pentru a reduce sarcina procesorului și, în special pentru conservarea energiei acumulatorului pe dispozitive mobile. În cazul nostru, vom crea o lume cu gravitație, ce va atrage corpurile cu 9.81 metri pe secundă la pătrat, care este aceeași accelerație cu cea de pe pământ. După ce lumea Box2D este creată, voi crea corpul corespunzător Box2D.
Box2D impune utilizarea de clase de definiție diferite pentru a crea noi instanțe de corpuri Body și de prinderi (Fixture), care sunt numite BodyDef și respectiv FixtureDef. Instanța bodyDef este configurată pentru a descrie un tip de corp cinematic a cărui poziție inițială este setată la aceeași poziție cu instanța obiectului vacuum. După aceasta se apelează metoda createBody() ce ține de variabila b2world și se transmite definiția corpului ca și parametru utilizat pentru a crea și a adăuga noul corp. Metoda returnează o referință a corpului nou creat, care este apoi stocat în instanța vacuum pentru a putea manipula fizica Box2D, în conformitate cu modificările aduse în metoda de update() a clasei abstracte AbstractGameObject pe care corpul o implementează.
Corpul creat are nevoie de o formă care să permită interacțiunea cu alte corpuri. Deci, vom crea o formă cu ajutorul clasei PolygonShape și prin apelarea metodei setAsBox() vom defini forma ca dreptunghi. Formele nu pot fi atașate direct la corpuri; astfel, vom crea o nouă instanță a clasei Fixture, care leagă forma mea de ea și în cele din urmă atașez prinderea fixture la corpul vacuum prin apelarea metodei de instanță a corpului createFixture(). Acum, forma nu mai este necesară deoarece informațiile sale au fost prelucrate în noua prindere. Acesta este motivul pentru care putem apela în siguranță metoda dispose() pentru forma (shape) cu scopul de a elibera memoria, care a fost alocată pentru această formă.
În Figura 5.16 este evidențiată folosirea corpurilor de tip Box2D:
Figura 5.16 Corpurile tip box2d a lumii jocului Equilibrium
Liniile subțiri albastre care formează un dreptunghi sau cele verzi pentru cercuri sunt menite pentru vizualizarea cum (forma) și unde (poziția) Box2D vede fiecare corp. Aceste linii au fost redate cu ajutorul clasei Box2DDebugRenderer ce ține de Box2D.
În cazul corpurilor dinamice, prindere (fixture) poate avea setate anumite proprietăți de material cum ar fi o anumită valoare pentru densitate, care afectează datele de masă calculate ale obiectului, și astfel, controlează dacă este vorba de un obiect greu sau un obiect ușor – de exemplu o minge de bowling are o densitate mare, dar un balon are o densitate mică, deoarece este umplut în princiu cu aer. În plus, elasticitatea (restitution) poate fi setată la o anumită valoare pentru a caracteriza un corp din punct de vedere al gradului de reacțiune în sens invers (asemeni unei sărituri) – de exemplu un bolovan va avea elasticitatea foarte mică, dar o minge de baschet va avea o elasticitate destul de mare. Un corp cu o elasticitate egală cu 0 se va opri deîndată ce atinge solul, în timp ce un corp cu o elasticitate egală cu 1 ar sări la aceeași înălțime pentru totdeauna. Frecarea (friction) este valoarea forței ce se opune atunci când obiectul se freacă/ alunecă de-a lungul unei suprafețe: un bloc de gheață ar avea o valoare de frecare foarte mică, dar o gumă de șters ar avea o valoare de frecare mare.
Un ultim aspect de care trebuie ținut cont, este adăugarea unei interfețe grafice de informare (Graphical User Interface) pentru șcena mea, care va suprapune lumea jocului. Graphical User Interface va face posibilă afișarea scorului jucătorului, numărului de vieți suplimentare, timpul de joc rămas, timpul rămas în cazul existenței la acel moment a unui comportament special cum ar fi: vagonul se comportă asemeni unui magnet pentru obiectele parașută cu cadou. Pentru utilizarea interfaței grafice de informare se va defini o nouă cameră cadru specializată de data aceasta doar pe afișarea unor informații și va fi definită în pixeli.
Pentru realizarea elementelor din interfața grafică de informare tip text va trebui să încărc un font bitmap înainte, pentru a putea scrie orice ieșire tip text pe ecran. Pentru implementarea acestui aspect trebuiesc copiate două fișiere de exemplu Arial-15.fnt și Arial-15.png ce definesc poziția fiecărei litere în cadrul unei imagini container (vezi Figura 5.17).
Figura 5.17 Implementare GUI (Graphical User Interface)
5.3.3 Realizarea meniurilor adiacente
În acest subcapitol, voi crea un meniu pentru jocul Equilibrium. Vor fi patru butoane din care jucătorul poate alege. Unul dintre butoane este Play, care va începe un joc nou, alt buton va fi pentru afișarea unui meniu de opțiuni care conține câteva setări ce pot fi schimbate, cum ar fi volumul pentru efectele de sunet și muzică, ori activarea vibrațiilor. Toate setările vor fi stocate și mai apoi încărcate dintr-un fișier de Preferințe pentru a le face permanent active în sesiunea de joc.
Așadar realizarea meniului va fi posibilă cu ajutorul unei șcene LibGDX numită Scene2D utilizată pentru a crea și organiza structuri de meniu complexe, precum și pentru a controla evenimentele din meniu, cum ar fi apăsarea butoanelor.
Meniul principal al jocului se va lansa în execuție după pornirea aplicației, iar pentru întoarcerea în acest ecran atunci când se revine din ecranul de joc mi-am definit următoare metodă care va fi apelată din două direcții: din meniul de pauză sau când utilizatorul apasă pe butonul înapoi al telefonului.
private void backToMenu () {
// switch to menu screen
game.setScreen(new MenuScreen(game));}
Acum că mi-am definit de unde ar putea fi accesat meniul principal mai rămâne decât să îl contruiesc. LibGDX vine cu un set foarte mare de caracteristici pentru a crea ușor o șcenă de acest tip care este o structură ierarhic organizată de obiecte similară cu cea a fișierelor și folderelor de pe un hard disk. În LibGDX, astfel de obiecte sunt numite actori. Actorii pot fi imbricați pentru a crea grupuri logice. Gruparea actorilor este o caracteristică foarte utilă, deoarece, de exemplu modificările aplicate unui actor părinte vor afecta actorii săi copii. În plus, fiecare actor are propriul sistem local de coordonate, ceea ce face foarte ușoarã definirea pozițiilor relative în interiorul unui grup de actori, unghiul de rotație și de scară.
Scene2D suportă detectarea atingerii, rotirii și scalării actorilor. Sistemul flexibil al LibGDX permite controlul intrărilor după cum este necesar, astfel încât actorii-părinte pot intercepta intrările înaintea actorilor copil. În final, sistemul de acțiune încorporat poate fi folosit pentru a manipula cu ușurință actori într-o perioadă de timp, creînd efecte complexe, care se pot executa secvențial, paralel sau într-o combinație a celor două. Toate aceste funcționalități descrise sunt încapsulate în clasa Stage. Clasa Stage și clasa Actor conțin o metodă act(), care are un timp delta ca argument pentru a realiza o acțiune bazată pe timp. Apelarea metodei act() a unei instanțe Stage va provoca un apel al metodei act() pe fiecare actor din acea scenă. Metoda act() este defapt similară comportamentului metodei update() despre care am discutat.
Datorită complexității elementelor, în vederea creãrii interfeței de utilizare a meniului voi utiliza clasa Scene2D UI. Această clasă furnizează un set bogat de elemente UI (User Interface) comun. În LibGDX, aceste elemente UI sunt numite widgets. Toate widget-urile disponibile în prezent în Scene2D UI sunt: Button, ButtonGroup, CheckBox, Dialog, Image, ImageButton, Label, List, ScrollPane, SelectBox, Slider, SplitPane, Stack, Window, TextButton, TextField, TextArea, Touchpad și Tree.
În plus față de clasa Scene2D UI, LibGDX include și o clasă destinată aranjării elementelor numită TableLayout. Obiectul TableLayout face foarte ușoră crearea dar și menținerea dinamică a șabloanelor (layouts) folosind tabele.
Ecranul meniului principal va fi compus dintr-o șcenă asemănătoare celei descrise mai sus, având un background special ales, butoanele mai sus menționate fiind ancorate în partea de jos a ecranului, iar logo-ul jocului aflându-se în poziție centrală. Din punct de vedere al aranjării în ecran voi folosi un șablon tip tabel, iar toate acțiunile asupra butoanelor vor avea atât un efect vizual cât și un efect practic funcție de butonul selectat.
Așadar, la început șcena din meniul principal începe cu o instanță Stage goală. Apoi, primul copil actor adăugat la instanța Stage este un widget tip stivă Stack. Widget-ul stivă permite adăugarea actorilor care pot suprapune alți actori, iar în cazul meu voi folosi această proprietate pentru a-mi crea mai multe straturi. Fiecare strat folosește un widget de tip tabel Table ca și părintele actor. Mai jos este exemplificat acest lucru
stage.clear();
Stack stack = new Stack();
stage.addActor(stack);
stack.add(buildLogosLayer ());
Astfel, de exemplu pentru realizarea actorului copil răspunzător de logo-ul jocului, îmi voi crea un strat logo ancorat central urmând pașii din metoda buildLogosLayer().
private Table buildLogosLayer () {
Table layer = new Table();
layer.center.top();
imgLogo = new Image(skinEquilibrium, "logo");
layer.add(imgLogo);
layer.row().expandY();
return layer;
}
În această manieră împreună cu alte metode complexe mi-am putut realiza toate meniurile adiacente ale jocului cum ar fi: meniul de pauză, meniul de setări, meniul de statistici, meniul de sfârșit de joc, dar și meniul principal care este prezentat în Figura 5.18.
Figura 5.18 Meniul Principal al jocului
Capitolul 6. Concluzii
Acest ultim capitol prezintă rezultatele obținute și măsura în care obiectivele propuse au fost atinse. De asemenea, vor fi enumerate și dezvoltările ulterioare posibile pentru îmbunătățirea aplicației.
Aplicația Equilibrium a fost concepută în maniera în care să se preteze a fi un mediu cât mai relaxant și prietenos cu utilizatorul, de aceea față de aplicațiile clasice, nivelul de calitate al aplicațiilor mobile de tip joc trebuie să fie superior datorită numeroaselor constrângeri impuse de dispozitivele mobile care sunt limitate atât din punct de vedere al rezolvării anumitor operații precum și a capabilităților de randare a imaginilor.
Deoarece aplicațiile destinate dispozitivelor mobile prezintă anumite particularități precum diversitatea mare de terminale pe care să poată fi capabile să ruleze în condiții optime, presupune din partea aplicației o calitate superioară. Diversitatea dispozitivelor mobile este asemenea diversității utilizatorilor finali și de aceea se impune ca aplicația sa aibă un caracter cât mai prietenos cu module care să poată fi accesate cu ușurință pentru toți utilizatorii.
Aplicația a trebuit să implementeze anumite tehnici și metode de optimizare special concepute astfel încât la utilizatorul final, necontând dispozitivul mobil utilizat, să ajungă același flux al ciclului de joc neîngrădit. Deoarece acest joc fundamentează decizii în timp real asupra elementelor din joc ce sunt concepute respectând legile fizicii și având o comportare asemeni obiectelor din lumea reală, se impune o corectitudine dar și o rapiditate în cadrul calculării operațiilor și transpunerea rezultatului acestora la ecranul dispozitivului mobil.
Printre realizările de natură tehnică se numără învățarea dezvoltării de aplicații mobile pe platforma Android utilizând motorul de joc dedicat Libgdx cu toate librăriile anexe ce au făcut posibilă realizarea prezentului joc.
Sistemul dezvoltat poate fi îmbunătățit prin adăugarea de noi funcționalități, dar și prin îmbunătățirea funcționalităților deja dezvoltate. Așadar îmi propun ca pe viitor să îmbunătățesc în primul rând caracterul prietenos al aplicației prin utilizarea unei grafici și mai de calitate care să atragă cât mai mulți utilizatori. Mai mult, îmi propun să adaug mai multe elemente ce pot fi colectate pentru a face jocul și mai interesant, iar pentru a face jocul mai competitiv îmi propun să implementez clasamentul global oferit de Google.
În concluzie, produsul final este o aplicație Android care poate fi utilizată de către orice persoană care deține un telefon mobil inteligent cu sistem de operare Android și care dorește să ia o pauză jucându-se un joc captivant care susține ideea de autodepășire.
Capitolul 7. Bibliografie
[1] „Industry Leaders Announce Open Platform for Mobile Devices” 5 noiembrie 2007. http://www.openhandsetalliance.com/press_110507.html
[2] „Google's Android parts ways with Java industry group” 13 noiembrie 2007 http://www.cnet.com/news/googles-android-parts-ways-with-java-industry-group/
[3] ,,Fundamentals of the Android architecture and terminologies” 11 Iunie 2012
http://www.eetimes.com/document.asp?doc_id=1279698
[4] ,, Google Open-Sources Android on Eve of G1 Launch” 21 octombrie 2008
http://www.eweek.com/c/a/Mobile-and-Wireless/Google-Open-Sources-Android-on-Eve-of-G1-Launch
[5] ,,The Complete Guide to Google Android” IDG Communcations 2011
[6] ,,Android (operating system)” https://en.wikipedia.org/wiki/Android_(operating_system)
[7] Ben Cheng; Bill Buzbee (Mai 2010). ,,A JIT Compiler for Android's Dalvik VM"
[8] Chris Haseman 2008 ,,Android Essentials”
[9] ,,Tools Overview Android Developers” 21 Iulie 2009. http://developer.android.com/tools/help/emulator.html
[10] Andreas Oehlke (Septembrie 2013) ,,Learning Libgdx Game Development”
[11] Suryakumar Balakrishnan Nair, Andreas Oehlke (Ianuarie 2015) ,,Learning Libgdx Game Development Second Edition”
[12] David Saltares Marquez, Alberto Cejas Sanchez (Octombrie 2014),,Libgdx Cross-platform Game Development Cookbook”
[13] Juwal Bose (Decembrie 2014) ,,Libgdx Game Development Essentials”
Capitolul 8. Anexe
În acest capitol este redat codul sursă al jocului
public class AndroidLauncher extends AndroidApplication {
@Override
protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
config.useAccelerometer = true;
initialize(new EquilibriumMain(), config);
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="ro.blutec.equilibrium.android"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="21" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/GdxTheme">
<activity
android:name="ro.blutec.equilibrium.android.AndroidLauncher"
android:label="@string/app_name"
android:screenOrientation="sensorLandscape"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
package ro.blutec.equilibrium.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.AssetDescriptor;
import com.badlogic.gdx.assets.AssetErrorListener;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import ro.blutec.equilibrium.util.Constants;
public class Assets implements Disposable, AssetErrorListener {
public static final String TAG = Assets.class.getName();
public static final Assets instance = new Assets();
private AssetManager assetManager;
public AssetFonts fonts;
public AssetLevelDecoration levelDecoration;
public AssetCart cart;
public AssetSounds sounds;
public AssetMusic music;
public AssetGifts gifts;
// singleton: prevent instantiation from other classes
private Assets () {
}
public class AssetFonts {
public final BitmapFont defaultSmall;
public final BitmapFont defaultNormal;
public final BitmapFont defaultBig;
public AssetFonts () {
// create three fonts using Libgdx's built-in 15px bitmap font
defaultSmall = new BitmapFont(Gdx.files.internal("images/arial-15.fnt"), true);
defaultNormal = new BitmapFont(Gdx.files.internal("images/arial-15.fnt"), true);
defaultBig = new BitmapFont(Gdx.files.internal("images/arial-15.fnt"), true);
// set font sizes
defaultSmall.setScale(0.75f);
defaultNormal.setScale(1.0f);
defaultBig.setScale(2.0f);
// enable linear texture filtering for smooth fonts
defaultSmall.getRegion().getTexture().setFilter(TextureFilter.Linear, TextureFilter.Linear);
defaultNormal.getRegion().getTexture().setFilter(TextureFilter.Linear, TextureFilter.Linear);
defaultBig.getRegion().getTexture().setFilter(TextureFilter.Linear, TextureFilter.Linear);
}
}
public class AssetSounds {
public final Sound play;
public AssetSounds (AssetManager am) {
play = am.get("sounds/play.ogg", Sound.class);
}
}
public class AssetMusic {
public final Music song01;
public AssetMusic (AssetManager am) {
song01 = am.get("music/game.ogg");
}
}
public class AssetLevelDecoration {
public final AssetRailway railWay;
public final AssetBeam beam;
public final TextureAtlas.AtlasRegion railWaySupport;
public final TextureAtlas.AtlasRegion pulley;
public final TextureAtlas.AtlasRegion rope;
public final Animation animKnobSmall;
public final Animation animKnobBig;
public AssetLevelDecoration (TextureAtlas atlas) {
railWay = new AssetRailway(atlas);
beam = new AssetBeam(atlas);
railWaySupport = atlas.findRegion("railway_support");
pulley = atlas.findRegion("pulley");
rope = atlas.findRegion("rope");
//Knob
Array<TextureAtlas.AtlasRegion> regions = null;
TextureAtlas.AtlasRegion region = null;
// Animation: Bunny Normal
regions = atlas.findRegions("knobSmall");
animKnobSmall = new Animation(1.0f / 10.0f, regions, Animation.PlayMode.NORMAL);
regions = atlas.findRegions("knobBig");
animKnobBig = new Animation(1.0f / 10.0f, regions, Animation.PlayMode.NORMAL);
}
public class AssetRailway {
public final TextureAtlas.AtlasRegion edge;
public final TextureAtlas.AtlasRegion middle;
public AssetRailway (TextureAtlas atlas) {
edge = atlas.findRegion("railway_edge");
middle = atlas.findRegion("railway_middle");
}
}
public class AssetBeam {
public final TextureAtlas.AtlasRegion edge;
public final TextureAtlas.AtlasRegion middle;
public AssetBeam (TextureAtlas atlas) {
edge = atlas.findRegion("beam_edge");
middle = atlas.findRegion("beam_edge");
}
}
}
public class AssetCart {
public final TextureAtlas.AtlasRegion cart;
public final TextureAtlas.AtlasRegion wheel;
public AssetCart (TextureAtlas atlas) {
cart = atlas.findRegion("cart");
wheel = atlas.findRegion("wheel");
}
}
public class AssetGifts {
public final TextureAtlas.AtlasRegion parachute;
public final TextureAtlas.AtlasRegion diamondBox;
public final TextureAtlas.AtlasRegion magnetBox;
public final TextureAtlas.AtlasRegion timeExtraBox;
public final TextureAtlas.AtlasRegion vacuumBox;
public final TextureAtlas.AtlasRegion bombBox;
public final TextureAtlas.AtlasRegion lifeExtraBox;
public final TextureAtlas.AtlasRegion doubleValueBox;
public final TextureAtlas.AtlasRegion immortalBox;
public AssetGifts (TextureAtlas atlas) {
parachute = atlas.findRegion("parachute");
diamondBox = atlas.findRegion("diamondBox");
magnetBox = atlas.findRegion("magnetBox");
timeExtraBox = atlas.findRegion("timeExtraBox");
vacuumBox = atlas.findRegion("vacuumBox");
bombBox = atlas.findRegion("bombBox");
lifeExtraBox = atlas.findRegion("lifeExtraBox");
doubleValueBox = atlas.findRegion("doubleValueBox");
immortalBox = atlas.findRegion("immortalBox");
}
}
public void init (AssetManager assetManager) {
this.assetManager = assetManager;
// set asset manager error handler
assetManager.setErrorListener(this);
// load texture atlas
assetManager.load(Constants.TEXTURE_ATLAS_OBJECTS, TextureAtlas.class);
// load sounds
assetManager.load("sounds/play.ogg", Sound.class);
// load music
assetManager.load("music/game.ogg", Music.class);
// start loading assets and wait until finished
assetManager.finishLoading();
Gdx.app.debug(TAG, "# of assets loaded: " + assetManager.getAssetNames().size);
for (String a : assetManager.getAssetNames()) {
Gdx.app.debug(TAG, "asset: " + a);
}
TextureAtlas atlas = assetManager.get(Constants.TEXTURE_ATLAS_OBJECTS);
// create game resource objects
fonts = new AssetFonts();
sounds = new AssetSounds(assetManager);
music = new AssetMusic(assetManager);
levelDecoration = new AssetLevelDecoration(atlas);
cart = new AssetCart(atlas);
gifts = new AssetGifts(atlas);
}
@Override
public void dispose () {
assetManager.dispose();
fonts.defaultSmall.dispose();
fonts.defaultNormal.dispose();
fonts.defaultBig.dispose();
}
@Override
public void error(AssetDescriptor asset, Throwable throwable) {
// TODO Auto-generated method stub
}
}
public abstract class AbstractGameScreen implements Screen {
protected DirectedGame game;
public AbstractGameScreen (DirectedGame game) {
this.game = game;
}
public abstract void render (float deltaTime);
public abstract void resize (int width, int height);
public abstract void show ();
public abstract void hide ();
public abstract void pause ();
public abstract InputProcessor getInputProcessor ();
public void resume () {
Assets.instance.init(new AssetManager());
}
public void dispose () {
Assets.instance.dispose();
}
}
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import ro.blutec.equilibrium.screens.transitions.ScreenTransition;
public abstract class DirectedGame implements ApplicationListener {
private boolean init;
private AbstractGameScreen currScreen;
private AbstractGameScreen nextScreen;
private FrameBuffer currFbo;
private FrameBuffer nextFbo;
private SpriteBatch batch;
private float t;
private ScreenTransition screenTransition;
public void setScreen(AbstractGameScreen screen) {
setScreen(screen, null);
}
public void setScreen(AbstractGameScreen screen,
ScreenTransition screenTransition) {
int w = Gdx.graphics.getWidth();
int h = Gdx.graphics.getHeight();
if (!init) {
currFbo = new FrameBuffer(Format.RGB888, w, h, false);
nextFbo = new FrameBuffer(Format.RGB888, w, h, false);
batch = new SpriteBatch();
init = true;
}
// start new transition
nextScreen = screen;
nextScreen.show(); // activate next screen
nextScreen.resize(w, h);
nextScreen.render(0); // let next screen update() once
if (currScreen != null)
currScreen.pause();
nextScreen.pause();
Gdx.input.setInputProcessor(null); // disable input
this.screenTransition = screenTransition;
t = 0;
}
public void init() {
}
@Override
public void render() {
// get delta time and ensure an upper limit of one 60th second
float deltaTime = Math.min(Gdx.graphics.getDeltaTime(), 1.0f / 60.0f);
if (nextScreen == null) {
// no ongoing transition
if (currScreen != null)
currScreen.render(deltaTime);
} else {
// ongoing transition
float duration = 0;
if (screenTransition != null)
duration = screenTransition.getDuration();
t = Math.min(t + deltaTime, duration);
if (screenTransition == null || t >= duration) {
// no transition effect set or transition has just finished
if (currScreen != null)
currScreen.hide();
nextScreen.resume();
// enable input for next screen
Gdx.input.setInputProcessor(nextScreen.getInputProcessor());
// switch screens
currScreen = nextScreen;
nextScreen = null;
screenTransition = null;
} else {
// render screens to FBOs
currFbo.begin();
if (currScreen != null)
currScreen.render(deltaTime);
currFbo.end();
nextFbo.begin();
nextScreen.render(deltaTime);
nextFbo.end();
// render transition effect to screen
float alpha = t / duration;
screenTransition.render(batch, currFbo.getColorBufferTexture(),
nextFbo.getColorBufferTexture(), alpha);
}
}
}
@Override
public void resize(int width, int height) {
if (currScreen != null)
currScreen.resize(width, height);
if (nextScreen != null)
nextScreen.resize(width, height);
}
@Override
public void pause() {
if (currScreen != null)
currScreen.pause();
}
@Override
public void resume() {
if (currScreen != null)
currScreen.resume();
}
@Override
public void dispose() {
if (currScreen != null)
currScreen.hide();
if (nextScreen != null)
nextScreen.hide();
if (init) {
currFbo.dispose();
currScreen = null;
nextFbo.dispose();
nextScreen = null;
batch.dispose();
init = false;
}
}
}
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Stack;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.utils.TimeUtils;
import com.badlogic.gdx.utils.viewport.FillViewport;
import ro.blutec.equilibrium.util.Constants;
public class SplashScreen extends AbstractGameScreen {
private static final String TAG = SplashScreen.class.getName();
private Stage stage;
private Skin skinDisney;
long startTime;
// menu
private Image imgLogo;
boolean splashScreenFinish=false;
public SplashScreen(DirectedGame game) {
super(game);
}
@Override
public void render(float deltaTime) {
Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
stage.act(deltaTime);
stage.draw();
if (!splashScreenFinish&&TimeUtils.millis()>(startTime+100)) {
splashScreenFinish=true;
game.setScreen(new MenuScreen(game));
}
}
@Override
public void resize(int width, int height) {
stage.getViewport().update(width, height, false);
}
@Override
public void show() {
startTime = TimeUtils.millis();
stage = new Stage(new FillViewport(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT));
rebuildStage();
}
@Override
public void hide() {
stage.dispose();
skinDisney.dispose();
}
@Override
public void pause() {
// TODO Auto-generated method stub
}
@Override
public InputProcessor getInputProcessor() {
return stage;
}
private void rebuildStage () {
skinDisney = new Skin(Gdx.files.internal(Constants.SKIN_EQUILIBRIUM), new TextureAtlas(Constants.TEXTURE_ATLAS_UI));
// build all layers
Table layerLogos = buildLogosLayer();
stage.clear();
Stack stack = new Stack();
stage.addActor(stack);
stack.setSize(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT);
stack.add(layerLogos);
}
private Table buildLogosLayer () {
Table layer = new Table();
layer.top();
// + Game Logo
imgLogo = new Image(skinDisney, "logo");
layer.add(imgLogo);
layer.row().expandY();
return layer;
}
}
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
public abstract class AbstractGameObject {
public Vector2 position;
public Vector2 dimension;
public Vector2 origin;
public Vector2 scale;
public float rotation;
public Vector2 velocity;
public Vector2 terminalVelocity;
public Vector2 friction;
public Vector2 acceleration;
public Rectangle bounds;
public Body body;
public float stateTime;
public Animation animation;
public AbstractGameObject() {
position = new Vector2(0,10);
dimension = new Vector2(30, 30);
origin = new Vector2();
scale = new Vector2(1, 1);
rotation = 0;
velocity = new Vector2();
terminalVelocity = new Vector2(1, 1);
friction = new Vector2();
acceleration = new Vector2();
bounds = new Rectangle();
}
public void update (float deltaTime) {
stateTime += deltaTime;
if (body == null) {
updateMotionX(deltaTime);
updateMotionY(deltaTime);
// Move to new position
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
} else {
position.set(body.getPosition());
bounds.setPosition(position.x – origin.x,position.y-origin.y);
rotation = body.getAngle() * MathUtils.radiansToDegrees;
}
}
protected void updateMotionX (float deltaTime) {
if (velocity.x != 0) {
// Apply friction
if (velocity.x > 0) {
velocity.x = Math.max(velocity.x – friction.x * deltaTime, 0);
} else {
velocity.x = Math.min(velocity.x + friction.x * deltaTime, 0);
}
}
// Apply acceleration
velocity.x += acceleration.x * deltaTime;
// Make sure the object's velocity does not exceed the
// positive or negative terminal velocity
velocity.x = MathUtils.clamp(velocity.x, -terminalVelocity.x, terminalVelocity.x);
}
protected void updateMotionY (float deltaTime) {
if (velocity.y != 0) {
// Apply friction
if (velocity.y > 0) {
velocity.y = Math.max(velocity.y – friction.y * deltaTime, 0);
} else {
velocity.y = Math.min(velocity.y + friction.y * deltaTime, 0);
}
}
// Apply acceleration
velocity.y += acceleration.y * deltaTime;
// Make sure the object's velocity does not exceed the
// positive or negative terminal velocity
velocity.y = MathUtils.clamp(velocity.y, -terminalVelocity.y, terminalVelocity.y);
}
public void setAnimation (Animation animation) {
this.animation = animation;
stateTime = 0;
}
public abstract void render (SpriteBatch batch);
}
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import ro.blutec.equilibrium.game.objects.AbstractGameObject;
public class CameraHelper {
private static final String TAG = CameraHelper.class.getName();
public final float MAX_ZOOM_IN = 0.25f;
public final float MAX_ZOOM_OUT = 1.3f;
public final float DEFAULT_ZOOM = .75f;
private final float FOLLOW_SPEED = 4.0f;
private Vector2 position;
private float zoom;
private AbstractGameObject target;
private OrthographicCamera camera;
public static float MAX_Y = 1.8f;
public static float MAX_X = 1.5f;
public CameraHelper() {
position = new Vector2();
zoom = .7f;
}
public void update (float deltaTime) {
if (!hasTarget()) return;
position.lerp(target.position, FOLLOW_SPEED * deltaTime);
/* if (zoom<1.0f){
MAX_Y = 0.5f;
} else if (zoom>1.0f && zoom<2.0f){
MAX_Y = 1.8f;
}*/
this.MAX_Y = MathUtils.clamp(position.y, 0.5f, 1.0f);
this.MAX_X = MathUtils.clamp(position.x, 0.8f, 1.85f);
// Prevent camera from moving vertical too far
if(position.y<0)
position.y = Math.max(-MAX_Y, position.y);
else
position.y = Math.min(MAX_Y, position.y);
// Prevent camera from moving lateral too far
if(position.x<0)
position.x = Math.max(-MAX_X, position.x);
else
position.x = Math.min(MAX_X, position.x);
}
public void setPosition (float x, float y) {
this.position.set(x, y);
}
public Vector2 getPosition () {
return position;
}
public void addZoom (float amount) {
setZoom(zoom + amount);
}
public void subtractZoom (float amount) {
setZoom(zoom – amount);
}
public void setZoom (float zoom) {
this.zoom = MathUtils.clamp(zoom, MAX_ZOOM_IN, MAX_ZOOM_OUT);
}
public float getZoom () {
return zoom;
}
public void setTarget (AbstractGameObject target) {
this.target = target;
}
public AbstractGameObject getTarget () {
return target;
}
public boolean hasTarget () {
return target != null;
}
public boolean hasTarget (AbstractGameObject target) {
return hasTarget() && this.target.equals(target);
}
public void applyTo (OrthographicCamera camera) {
camera.position.x = position.x;
camera.position.y = position.y;
camera.zoom = zoom;
camera.update();
this.camera=camera;
}
public void resetZoom(){
zoom = DEFAULT_ZOOM;
}
public OrthographicCamera getCamera(){
return camera;
}
}
public class Constants {
// Visible game world is 10 meters wide
public static final float VIEWPORT_WIDTH = 10.0f;
// Visible game world is 10 meters tall
public static final float VIEWPORT_HEIGHT = 15.0f;
public static final float HEIGHT_WORLD = VIEWPORT_HEIGHT *2.0f;
public static final float POSITIVE_HEIGHT_OFFSET = VIEWPORT_HEIGHT / 2.0f;
public static final float WORLD_WIDTH = 10.0f;
public static final float SPAWNING_POSITION_Y = VIEWPORT_HEIGHT/2.0f +1.0f;
public static final float SPAWNING_OFFSET = 1.4f;
// GUI Width
public static final float VIEWPORT_GUI_WIDTH = 800.0f;
// GUI Height
public static final float VIEWPORT_GUI_HEIGHT = 480.0f;
// Location of description file for texture atlas
public static final String TEXTURE_ATLAS_UI = "images/equilibrium-ui.pack";
public static final String TEXTURE_ATLAS_OBJECTS = "images/equilibrium.pack";
public static final String TEXTURE_ATLAS_LIBGDX_UI = "images/uiskin.atlas";
// Location of description file for skins
public static final String SKIN_LIBGDX_UI = "images/uiskin.json";
public static final String SKIN_EQUILIBRIUM = "images/equilibrium-ui.json";
// Game preferences file
public static final String PREFERENCES = "equilibrium.prefs";
// Angle of rotation for dead zone (no movement)
public static final float ACCEL_ANGLE_DEAD_ZONE = 5.0f;
// Max angle of rotation needed to gain maximum movement velocity
public static final float ACCEL_MAX_ANGLE_MAX_MOVEMENT = 20.0f;
public static final int LIVES_START = 2;
public static final int PLAY_TIME = 60;
}
public class GiftObjectUserData {
private boolean flagForDelete;
private boolean magnetized;
private boolean vacuumed;
private Gift.GiftType giftType;
public GiftObjectUserData(){
}
public boolean isFlagForDelete() {
return flagForDelete;
}
public void setFlagForDelete(boolean flagForDelete) {
this.flagForDelete = flagForDelete;
}
public Gift.GiftType getGiftType() {
return giftType;
}
public void setGiftType(Gift.GiftType giftType) {
this.giftType = giftType;
}
public boolean isMagnetized() {
return magnetized;
}
public void setMagnetized(boolean magnetized) {
this.magnetized = magnetized;
}
public boolean isVacuumed() {
return vacuumed;
}
public void setVacuumed(boolean vacuumed) {
this.vacuumed = vacuumed;
}
}
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
public class AudioManager {
public static final AudioManager instance = new AudioManager();
private Music playingMusic;
// singleton: prevent instantiation from other classes
private AudioManager () {
}
public void play (Sound sound) {
play(sound, 1);
}
public void play (Sound sound, float volume) {
play(sound, volume, 1);
}
public void play (Sound sound, float volume, float pitch) {
play(sound, volume, pitch, 0);
}
public void play (Sound sound, float volume, float pitch, float pan) {
if (!GamePreferences.instance.sound) return;
sound.play(volume, pitch, pan);
}
public void play (Music music) {
playingMusic = music;
if (GamePreferences.instance.music) {
music.setLooping(true);
music.play();
}
else
music.stop();
}
public void stopMusic () {
if (playingMusic != null) playingMusic.stop();
}
public Music getPlayingMusic () {
return playingMusic;
}
public void onSettingsUpdated () {
if (playingMusic == null) return;
playingMusic.setVolume(GamePreferences.instance.volMusic);
if (GamePreferences.instance.music) {
if (!playingMusic.isPlaying()) playingMusic.play();
} else {
playingMusic.pause();
}
}
}
Copyright Notice
© Licențiada.org respectă drepturile de proprietate intelectuală și așteaptă ca toți utilizatorii să facă același lucru. Dacă consideri că un conținut de pe site încalcă drepturile tale de autor, te rugăm să trimiți o notificare DMCA.
Acest articol: Specializare: Automatică și Informatică Aplicată PROIECT DE DIPLOMĂ COORDONATOR ȘTIINȚIFIC: Șef Lucrări dr. ing. Oana Niculescu – Faida ABSOLVENT… [305857] (ID: 305857)
Dacă considerați că acest conținut vă încalcă drepturile de autor, vă rugăm să depuneți o cerere pe pagina noastră Copyright Takedown.
