1
UNIVERSITATEA „ALEXANDRU IOAN CUZA” IAŞI
FACULTATEA DE INFORMATICĂ
LUCRARE DE LICENŢĂ
Dungeon Master – Joc de strategie tactic pe
platforma Android
Propusă de
Busuioc D. George Alexandru
Sesiunea: Februarie, 2017
Coordonator știinţific
Asistent, dr. Vasile Alaiba
2
UNIVERSITATEA „ALEXANDRU IOAN CUZA” IAŞI
FACULTATEA DE INFORMATICĂ
LUCRARE DE LICENŢĂ
Dungeon Master - Joc de strategie tactic pe
platforma Android
Propusă de
Busuioc D. George Alexandru
Sesiunea: Februarie, 2017
Coordonator știinţific
Asistent, dr. Vasile Alaiba
3
DECLARAŢIE PRIVIND ORIGINALITATE ŞI RESPECTAREA
DREPTURILOR DE AUTOR
Prin prezenta declar că Lucrarea de licență cu titlul „Dungeon Master” este scrisă de mine şi nu a
mai fost prezentată niciodată la o altă facultate sau instituţie de învățământ superior din ţară sau
străinătate. De asemenea, declar că toate sursele utilizate, inclusiv cele preluate de pe Internet,
sunt indicate în lucrare, cu respectarea regulilor de evitare a plagiatului:
− toate fragmentele de text reproduse exact, chiar şi în traducere proprie din altă limbă,
sunt scrise între ghilimele şi deţin referinţa precisă a sursei;
− reformularea în cuvinte proprii a textelor scrise de către alţi autori deţine referinţa
precisă;
− codul sursă, imagini etc. preluate din proiecte open source sau alte surse sunt utilizate
cu respectarea drepturilor de autor şi deţin referinţe precise;
− rezumarea ideilor altor autori precizează referinţa precisă la textul original.
Iaşi,
Absolvent Busuioc George Alexandru
_________________________
(semnătura în original)
4
DECLARAŢIE DE CONSIMŢĂMÂNT
Prin prezenta declar că sunt de acord ca Lucrarea de licență cu titlul „ Dungeon Master”, codul
sursă al programelor şi celelalte conţinuturi (grafice, multimedia, date de test etc.) care însoţesc
această lucrare să fie utilizate în cadrul Facultăţii de Informatică. De asemenea, sunt de acord ca
Facultatea de Informatică de la Universitatea „Alexandru Ioan Cuza” Iași să utilizeze, modifice,
reproducă şi să distribuie în scopuri necomerciale programele-calculator, format executabil şi
sursă, realizate de mine în cadrul prezentei lucrări de licenţă.
Iaşi,
Absolvent Busuioc George Alexandru
_________________________
(semnătura în original)
5
Cuprins
1. Introducere .............................................................................................................................................6
1.1 Motivație ........................................................................................................................................6
1.2 Context ...........................................................................................................................................7
1.3 Aplicații asemănătoare ...................................................................................................................8
1.4 Cerințe funcționale .......................................................................................................................12
1.5 Abordare tehnică ..........................................................................................................................13
2. Contribuții ............................................................................................................................................15
3. Proiectare ..............................................................................................................................................17
3.1 Arhitectura soluției .......................................................................................................................17
3.2 Modelarea datelor .........................................................................................................................22
3.2.1 Modelarea datelor pe aplicația client ....................................................................................22
3.2.2 Modelarea datelor pe server .................................................................................................27
3.3 Interfața cu utilizatorul .................................................................................................................28
4. Implementare ........................................................................................................................................33
5. Manual de utilizare ...............................................................................................................................45
5.2 Meniul principal ...........................................................................................................................45
5.3 Meniul de creare a unui erou ........................................................................................................46
5.4 Interfața utilizatorului ...................................................................................................................48
5.5 Desfășurarea unui joc ...................................................................................................................49
6. Concluzii ..............................................................................................................................................54
7. Referințe ...............................................................................................................................................55
6
1. Introducere
1.1 Motivație
Consider că fiecare dintre noi a jucat un joc de societate la un moment dat. Unul dintre cele
mai vechi dar încă actuale jocuri de societate este șahul. Chiar dacă aceste jocuri au farmecul lor,
de cele mai multe ori am prefera o variantă mai accesibilă, cu o durată mai scurtă de timp si care
să ne permită participarea la joc de la distanță.
Care ar fi problema sau neajunsul însă pentru un astfel de joc? Toate jocurile de acest tip au
nivelele deja prestabilite. Dacă nu ai reusit într-un astfel de joc să îți învingi adversarul, o vei lua
de la capăt iar modul în care se va desfășura jocul se va repeta. Vei încerca noi tactici si vei
continua să faci acest lucru până vei reuși să descoperi o strategie prin care să câștigi. De aici
reiese faptul că odată ce a fost descoperită o astfel de strategie și vei decide să reiei jocul de la
acel nivel, dificultatea va fi semnificativ redusă deoarece strategia este deja știută și astfel jocul
nu mai oferă nici o satisfacție. De luat în considerare este faptul că uneori căutarea unei strategii
poate deveni o rutină iar jucătorul își va pierde răbdarea, ceea ce va duce la abandonarea
completă a jocului.
Una din soluții ar fi creearea unui joc care de fiecare dată cand este jucat să ofere alte
provocări față de jocul precedent, alte specificații sau o altă abordare. Această soluție presupune
un joc care stimulează capacitatea jucătorului de a lua decizii rapide la fiecare moment și fără
posibilitatea de a planifica totul din timp. Acest lucru duce la explorarea mai multor strategii în
timpul jocului și adaptarea lor la fiecare pas.
Să luăm exemplul unui joc de defensivă în care jucătorul trebuie să împiedice inamicii să
ajungă într-un anumit punct. În primă fază jucătorul va lua pe ghicite anumite decizii pentru a
observa ce strategie îl va duce la câștigarea jocului. În cazul în care jucătorul va pierde, va relua
nivelul precedent si va încerca să modifice strategia anterioară acolo unde consideră că nu a fost
optimă. Inamicii vor veni în aceeași ordine, la aceleași intervale de timp și în același număr de
fiecare dată când nivelul este jucat. Astfel pentru a ajunge la o strategie care să conducă spre
victorie sunt necesare mai multe instanțe ale aceluiași nivel. Acest lucru va face ca jucătorul să își
piardă răbdarea după câteva eșecuri, având în vedere faptul că a trebuit sa repete aceleași
evenimente de mai multe ori fără succes.
7
Pe de altă parte, dacă punctul care trebuie apărat își modifică poziția la fiecare instanță a
nivelului, va obliga jucătorul să găsească strategii noi pentru jocul curent. Mai mult, cum ar fi
dacă ordinea inamicilor s-ar schimba puțin? Poate aceste lucruri vor veni în ajutorul jucătorului
sau poate îl vor împotmoli și mai mult. Cert este că nu știe ce îl așteaptă iar prin acest lucru se
menține atenția jucătorului și speranța lui că acest joc va fi acela în care strategia lui îl va
conduce la victorie.
1.2 Context
Conceptul de joc de societate a apărut încă de la începutul erei noastre având drept scop
divertismentul jucătorilor. Primele jocuri de societate erau competitive, scopul jocului fiind
eliminarea oponentului sau a oponenților. În timp s-a dezoltat și ideea de jocuri cooperative în
care jucatorii se aliază și creează o strategie pentru a înfrânge jocul. Jocurile de societate se joacă
pe o tablă de joc și urmăresc un set de reguli prestabilite.
Jocurile video au evoluat considerabil in ultimii ani devenind chair o adevarată industrie în
acest sens. Totodată acestea au căutat să se extindă și pe alte platforme, altele decât cele specifice
precum consolele video sau cele deja consacrate cum ar fi calculatorul, ajungând inevitabil si pe
dispozitivele mobile.
În prezent există diverse implementări ale jocurilor de societate, atât pentru jocuri
consacrate precum Monopoly sau Șah, cât și pentru jocuri mai puțin cunoscute. Acestea își
propun să păstreze cât mai multe caracteristici din versiunea originală a jocului dar totodată să
utilizeze avantajele dispozitivului pentru care sunt destinate.
Întrucăt mă consider un pasionat atât al jocurilor de societate cât și al jocurilor video, am
ales să dezvolt o aplicație bazată pe ture, inspirată din jocurile clasice Dungeons&Dragons în
care fiecare sesiune de joc este unică, relativ scurtă si poate fi accesată de oriunde deoarece este
disponibilă pe dispozitivul mobil sau tabletă.
8
1.3 Aplicații asemănătoare
Dungeon Hunter 51
Această aplicație este un joc pentru telefoane mobile și tablete dezvoltat de Gameloft care
urmărește evoluția unui erou în de tip ce caută să devină cel mai mare căutător de comori. Cadrul
jocului este unul fantastic și conține numeroase funcționalități care fac jucătorul mereu să se
întoarcă.
Jocul conține o campanie prestabilită unde jucătorul urmărește un lanț de nivele și unde
fiecare etapă este dependentă de cea de dinainte. Astfel, pentru a parcurge toată campania, acest
mod trebuie parcurs în ordine la cea mai mică dificultate. Pe parcurs se vor dezlega dificultăți mai
mari pentru același nivel. Acesta este un mod foarte bun de a-ți îmbunătăți eroul, în schimb jocul
devine repetitiv prin faptul că te forțează să revii în locurile deja parcurse.
Figura 1: Harta de campanie Dungeon Hunter 5
Fiecare nivel al jocului presupune ca eroul să ajungă la capătul lui eliminând toți inamicii
pe care îi întămpină folosind două sau trei abilități stabilite de obiectele echipate pe caracter.
Inamicii sunt mereu aceiași dar în funcție de dificultate ei pot fi mai slabi sau mai puternici. La
1 Pagina oficială Dungeon Hunter 5, http://www.dungeonhunter5.com/
9
fel ca în majoritatea jocurilor harta este mereu aceeași. În caz de eșec nivelul trebuie reluat de la
capăt și va urma același șir de evenimente.
Spre deosebire de marea majoritate a jocurilor pe mobil, acesta oferă posibilitatea de a cere
ajutorul unui aliat de a te asista în timpul jocului. Dezavantajul este că jucătorul aliat este
controlat de un robot și nu de persoana fizică a cărui personaj îi aparține, ceea ce duce la o
experiență mai scăzută a jocului atunci când vine vorba de joc de echipă.
Aplicația conține însă multe moduri prin care poți îmbunătăți eroul tău, începând de la
obiecte pe care le poți lua în timpul jocului până la diferite bonusuri obținute în timpul unor
evenimente organizate de producători. Ca și interacțiune online poți comunica cu persoanele de
pe server pe conversația publică sau cu persoanele înscrise din aceeași echipă.
Figura 2: Interfața și un exemplu de nivel al jocului
Kingdom Rush2
Kingdom Rush este o aplicație pentru mobil și tablete dezvoltată de Armor Games. Este un
joc popular de tip defensiv unde dintr-unul sau mai multe puncte vor pleca grupuri de inamici,
vor urmări o anumită cale iar scopul jucătorului este de a-i împiedica pe aceștia să ajungă la
destinație.
2 Pagina oficială Kingdom Rush http://www.kingdomrush.com/
10
Jocul se poate juca doar de către o singură persoană simultan și conține o campanie formată
din douăsprezece nivele. Fiecare nivel este diferit și cu cât se progresează pe hartă își vor face
apariția noi tipuri de inamici, culminând în anumite nivele cu un lider puternic. Împiedicarea
acestora se va face prin construirea de turnuri de diferite tipuri care vor elimina, bloca, încetini
sau teleporta în spate unitățile, urmărind calea.
Fiind un joc care nu necesită conexiune la internet tot progresul în această campanie se va
salva local în memoria telefonului sau a tabletei. Aplicația oferă trei astfel de spații unde se poate
salva progresul.
Figura 3: Un exemplu de nivel din Kingdom Rush
Jocul mai oferă posibilitatea de a chema un erou pe câmpul de luptă pentru a te ajuta. Acest
erou este controlabil de către jucător și se poate poziționa oriunde pe hartă, chiar și în calea
11
inamicilor. Dezavantajul este că acest erou își va folosi abilitățile doar când consideră el că este
necesar și nu la comandă. Totodată eroul este predefinit și nu poate fi modificat în niciun fel.
Numărul de douăsprezece nivele nu este unul tocmai satisfăcător având în vedere faptul că
fiecare dintre ele nu va aduce nimic deosebit față de precedentul. În schimb jocul oferă la fiecare
nivel parcurs două noi moduri în care poate fi terminat acel nivel. Fie inamicii vin într-un singur
grup, fie jocul provoacă jucătorul să construiască doar anumite tipuri de turnuri.
Figura 4: Selecția de eroi din Kingdom Rush
12
1.4 Cerințe funcționale
Jucătorul poate crea un nou erou bazat pe un set de rase și clase prestabilite de către joc și
salvarea lui local pentru a putea fi folosit în viitoarele jocuri. Pe ecran se va putea selecta rasa
eroului și în funcție de această selecție se va putea selecta și specializarea. Acestea vor influența
atributele caracterului iar numărul maxim de eroi este 5.
Jucătorul are posibilitatea de a șterge oricare dintre eroii deja creați de el.
Jucătorul are posibilitatea alegerii de a juca singur împotriva jocului sau de a forma o
echipă cu alți doi jucători pentru a-l înfrânge împreună. Pentru jocul în echipă este necesară o
conexiune la internet pentru a se conecta la un server, spre deosebire de cel singur unde nu este
nevoie.
Utilizatorul va putea crea un erou la începutul fiecărui joc. Jucătorul va avea o listă de eroi
deja creați și va avea posibilitatea de a alege unul dintre ei pentru a fi folosit în următorul joc.
Totodată va fi prezent și un buton pentru crearea imediată a unui erou.
Va fi posibilă întreruperea jocului și afișarea unui meniu de pauză atunci când jucătorul
apasă pe butonul de pauză într-un joc cu un singur jucător.
În timp ce jocul este întrerupt jucătorul are opțiunea de abandonare a jocului și întoarcerea
la meniul principal, apasând pe butonul de abandon.
Pe parcursul jocului eroul poate fi mutat pe hartă folosind butonul de mișcare și selectând
pătratul destinație. Pătratul trebuie să fie în limita zonei în care eroul se poate mișca, determinat
de atributul viteză al acestuia.
Jucătorul poate decide să atace unul sau mai mulți inamici. Pentru aceasta el va folosi
butonul de atac, urmat de una dintre abilitățile eroului. În funcție de abilitate el poate ataca un
singur inamic sau o multitudine de inamici aflați în raza abilității.
Când jucătorul consideră că este necesar, poate folosi o abilitate de ajutor care va
reîmprospăta sau modifica anumite atribute ale eroului. Acestea diferă în funcție de abilitățile
alese și de specializarea caracterului în joc.
În orice moment se pot inspecta atributele oricărui erou sau inamic prezent pe tabla de joc.
Eroii și inamicii au moduri de prezentare diferite pentru a semnala informațiile relevante.
13
În cazul în care jucătorul și-a folosit toate acțiunile posibile ale eroului sau decide că nu
poate contribui cu nimic la această rundă, își va putea încheia tura.
Pentru fiecare acțiune care necesită o aruncare a zarului, jucătorul are opțiunea de a face
acest lucru prin butonul de aruncare a zarului.
Utilizatorul poate mișca poziția jocului astfel încât poate explora toată harta generată la fel
și o opțiune de a mări sau micșora obiectele de pe ecran.
1.5 Abordare tehnică
Aplicația folosește pentru partea de client framework-ul multiplatformă libGDX cu
limbajul de compilat în JAVA, iar pentru partea de server Node.js cu limbaj de scripting
JavaScript. Comunicarea între client și server pentru un joc cooperativ se va realiza prin
biblioteca Socket.IO.
LibGDX este un framework cu sursă deschisă pentru dezvoltarea de jocuri video scrisă în
limbajul de programare JAVA. Aceasta oferă posibilitatea de a dezvolta jocuri atât
bidimensionale cât și tridimensionale pe mai multe platforme precum Windows, Android, iOS
dar folosind același cod de bază. (1) Totodată oferă și anumite extensii pentru creearea de
concepte mult mai avansate decât cele din biblioteca standard.
Android SDK este un set de instrumente care facilitează crearea de aplicații specifice
sistemului de operare Android. Pachetul conține atât instrumente pentru debugging și testare, cât
și un emulator complet funcțional configurabil pentru orice dispozitiv Android. (2) Acest pachet
ajută framework-ul libGDX în rularea codului compilat pentru Android.
Java este un limbaj de programare orientat obiect și concurent dezvoltat inițial de Sun
Microsystems în anul 1995. Acesta a fost gândit ca cei care dezvoltă aplicații să poată scrie codul
o singură dată și să poată fi rulat pe orice platformă. (3) Acest lucru este posibil datorită unei
mașini virtuale care este capabilă să execute cod scris specific pentru mașina gazdă. Bibliotecile
standard facilitează un mod generic de a accesa caracteristicile mașinii gazdă precum grafica sau
rețeaua acesteia.
Node.js este un mediu de programare dezvoltat peste JavaScript engine V8 oferit de
Chrome cu un singur fir de execuție. Oferă psobilitatea de a crea servere flexibile bazate pe
14
evenimente și transfer de date asincron. Din această cauză server-ul Node.js nu va fi niciodată
blocat și este preferat pentru aplicațiile scalabile. (4)
JavaScript este un limbaj de programare de nivel înalt orientat pe obiecte, dinamic și
interpretat. Deși inițial acest limbaj a fost dezvoltat pentru a introduce noi funcționalități
paginilor web, în ziua de azi este folosit în aproape orice domeniu IT deoarece este un limbaj
ușor de utilizat. Acesta folosește un API pentru a lucra cu date de tip text, șiruri, date
calendaristice și expresii regulate, dar nu include componente de intrare sau ieșire precum
rețelistică sau grafică. Acest limbaj a fost standardizat, având prima ediție publicată in 1997. (5)
Socket.IO este o bibliotecă JavaScript pentru aplicații în timp real. Aceasta facilitează
comunicare bidirecțională între clienți și conține două componente, biblioteca ce rulează pe
partea de client și biblioteca pentru server. Exact ca și Node.js, comunicarea este bazată pe
evenimente. (6)
15
2. Contribuții
Dungeon Master este un joc tridimensional bazat pe ture care urmărește evoluția unuia sau
a mai multor personaje care întâmpină pe parcursul jocului diferiți monștri și hazarde în incinta
unei temnițe înfățișată sub forma unei grile (eng: grid). Jucătorul are posibilitatea de a-și crea
propriul erou și de a porni într-o aventură. Eroul va avea un set de abilități prestabilite dintre care
jucătorul va putea alege. Fiecare aventură, fiecare monstru și fiecare temniță va fi unică pentru
fiecare joc generat pseudo-aleatoriu. Fiecare creatură eliminată va genera eroului sau grupului de
eroi puncte de experiență care pot fi folosite pentru a evita anumite hazarde. Acțiunea jocului va
avea loc pe o hartă împărțită în plăci (eng: tile) formate din mai multe pătrate (eng: squares).
Fiecare aventură va începe cu două plăci și restul se vor „descoperi” pe parcursul jocului (ca și
cum personajul ar merge prin întuneric). Scopul jocului este de a îndeplini obiectivul setat la
început. Obiectivele pot varia de la eliminarea unui monstru specific, până la descoperirea unui
artefact. Neîndeplinirea obiectivului sau eliminarea unuia dintre personaje va semanala încheierea
jocului.
Cele mai importante contribuții în acest sens sunt următoarele:
Generarea pseudo-aleatorie a plăcilor ce vor constitui harta jocului.
Generarea pseudo-aleatorie a inamicilor ce vor fi plasați pe hartă. Se vor lua totuși în
calcul numărul de jucători prezenți precum și specializările lor.
Implementarea unui server bazat pe evenimente pentru a facilita jocul de echipă în trei
jucători.
Pentru a atinge acest obiectiv am decis să împart proiectul în două componente, server și
client, și să le dezvolt separat. Totodată structura lucrării va urma aceeași manieră, fiecare capitol
va prezenta deciziile luate în cadrul dezvoltării precum și detalii de implementare exemplificate și
explicate.
Pe partea de client am folosit framework-ul libGDX pentru a realiza mecanismul necesar
jocului. Am ales această bibiliotecă deoarece este foarte intuitivă de folosit, oferă control asupra
multor funcționalități de nivel jos și se prezintă cu multe utilități pentru a îmbunătăți experiența
jucătorului.
16
Pe partea de server am ales să folosesc Node.js pentru gestionarea clienților într-un joc de
echipă. Alegerea aceasta a fost datorată sistemului asincron al serverului și al faptului că serverul
va hiberna dacă nu are de rulat nici o operație, aducând un plus de performanță.
Comunicarea între client și server va fi facilitată de către Socket.IO. Această alegere se
datorează faptului că biblioteca este bazată pe evenimente, la fel ca și serverul.
17
3. Proiectare
3.1 Arhitectura soluției
Aplicația este împarțită în două proiecte pentru fiecare capăt al comunicării, client și server. În
continuare voi prezenta arhitectura aplicației, ce se împarte în două componente descrise pe larg.
Figura 5: Arhitectura aplicației
Clientul este responsabil în mare parte de afișarea pe ecran a tuturor obiectelor vizuale ce
compun aplicația, dar totodată și de foarte multă logică ce ajută la desfășurarea jocului și
interacțiunea obiectelor între ele. Pentru randarea obiectelor tridimensionale pe ecran biblioteca
foloseste un motor OpenGL ES 2.0 și un wrapper pentru 3.0. (7) Proiectul conține trei module
principale:
1. Un modul standard Java peste care a fost aplicat framework-ul libGDX împreună
cu toate dependețele acestuia: ca orice modul Java acesta va avea nevoie de un mediu de
dezvoltare necesart rulării și compilării codului sursă. Pentru proiectul curent am folosit jre
1.8.0.
2. La crearea unui modul libGDX, fiind un framework în care se pot crea aplicații
multiplatformă, se pot specifica ce module pot fi instalate. În cazul de față am ales Android
și pentru acesta am ales ca și mediu de dezvoltare Android API 22 Platform. În acest modul
numit sugestiv „android” se pot specifica anumite dependențe și configurări specifice
mediului în care va fi rulat jocul. În cazul de față am adăugat permisiunea <uses-
permission android:name="android.permission.INTERNET" /> care îmi va facilita accesul
18
la internet. Tot aici am setat în activitatea aplicației ca modul de afișare să fie mereu pe
orizontală. Am luat această decizie pentru a avea o interfață mult mai utilă pentru utilizator.
Acest lucru l-am realizat prin adăugarea liniei
android:screenOrientation="sensorLandscape"
în activitatea principală a fișierului AndroidManifest.xml. De specificat faptul că
datorită unor constrângeri ale modului de interpretare Android, toate resursele folosite
în proiect trebuie încărcate în acest modul în directorul „assets”.
3. Ultimul modul este și cel mai important. Deoarece libGDX este un framework
multiplatformă, nu te va forța să scrii cod specific platformei destinație. Acesta va folosi
diferite operații backend pentru a accesa procesele specifice mașinii și pentru a compila și
porni codul sursă. În cazul nostru aceastea vor fi realizate cu ajutorul Android SDK. În
acest modul numit „core” se va scrie tot codul sursă al aplicației. Astfel libGDX urmează
sloganul „Write once, run anywhere” (scrii o singură dată, utilizezi peste tot). Codul sursă
este scris în JAVA și urmează principiile POO. De menționat însă este faptul că același cod
sursă ar fi putut fi scris și în modulul precedent, dar acest lucru ar fi făcut ca aplicația să
poată fi accesată doar de pe un dizpozitiv Android. Deoarece am vrut ca pe viitor să pot
extinde acest joc, codul sursă a fost scris în acest modul.
19
Figura 6: Modulele ce compun clientul
Trebuie menționat faptul că un framework dedicat jocurilor va gestiona diferit modulele
față de un API sau widget dedicat redării grafice. Astfel el va pune la dispoziție o metodă care va
fi mereu apelată la fiecare cadru randat de către aplicație. Deci pentru jocul nostru această metodă
va fi apelată de 60 de ori pe secundă. Aceasta va servi la modificarea poziției obiectelor pe ecran
și pentru ca jucătorul să fie mereu avertizat și ținut la curent de orice schimbare.
Pentru ca utilizatorul să se bucure de o experiență de joc mai dinamică am ales ca marea
majoritate a graficii jocului să fie compusă din obiecte tridimensionale, atât obiecte create
dinamic cât și unele încărcate la începerea jocului. Din fericire libGDX oferă destule
funcționalități de gestionare ale acestor obiecte, cât și redarea lor pe ecran. Având acel strat de
grafică putem crea obiecte detaliate, aplica diferite „modifiers” (procese ce manipulează obiectul)
și la final aplicarea unei texturi. Trebuie avut grijă însă că marea majoritate a performaței jocului
scade considerabil odată cu creșterea calității texturilor și modelelor folosite. De aceea marea
majoritate a modelelor și texturilor vor fi create la încărcarea jocului și folosite pe tot parcursul
20
lui atunci când este nevoie. Alte obiecte sunt create dinamic doar atunci când este nevoie după
care vor fi scoase din memorie. Toate aceste resurse au fost încărcate în folderul „assets”.
Un alt rol pe care îl are clientul este de a percepe orice atingere sau mișcare pe ecran și
trimiterea acestor evenimente către stratul de logică al jocului pentru a fi interpretate. În
continuare clientul va valida aceste evenimente și va răspunde corespunzător.
Un alt aspect de luat în considerare atunci când se dezvoltă un joc de echipă online (sau
orice aplicație client-server) este volumul de logică și validări care va trebui să fie împărțit între
client și server. Astfel o aplicație care va face validările pe server va fi mult mai robustă și
pregătită impotriva jucătorilor care vor încerca să trișeze. Pe de altă parte aplicațiile care au o
mare parte din validări pe partea de client, vor fi mai expuse trișorilor, dar se vor comporta mult
mai bine în cazul în care internetul tinde să aibe scăpări sau latență. Strict legat de dezvoltarea
unui astfel de joc putem totodată menționa că nu este eficient să se trimită la server orice input al
jucătorului pentru a fi validat, deoarece ar fi o irosire de bandă și timp. În continuarea aceste idei
este mai eficient ca un client să trimită doar anumite tag-uri la care serverul să reacționeze și să
trimită în continuare celorlalți clienți informația pentru a fi interpretată de fiecare în parte. Spre
exemplu dacă un jucător a fost eliminat în timpul unei runde, clientul va trimite un tag
«Player1Died» la server iar serverul va valida tag-ul și îl va trimite la ceilalți clienți unde fiecare
în parte va ști ce animații sau sunete să gestioneze. Având în vedere faptul că aplicația dezvoltată
de mine are și un mod single care nu necesită acces la internet ar fi trebuit să dezvolt aceleași
validări și pe client și pe server. Dat fiind timpul restrâns pentru dezvoltarea acestei aplicații, am
decis ca majoritatea validărilor să fie pe client.
Comunicarea între client și server va fi gestionată de un serviciu numit Socket.IO.
Aceasta conține două biblioteci specifice fiecărui capăt de comunicare, o bibliotecă pentru partea
de client și una pentru partea de server. Acest lucru este datorat faptului că deși această bibliotecă
este construită peste alte protocoale de transport în timp real, în timpul negocierilor de conexiune
va căuta mereu protocolul creat special pentru Socket.IO astfel ignorând protocoalele WebSocket
standardizate. În concluzie o conexiune folosind Socket.IO va funcționa doar când atât clientul,
cât și serverul vor folosi respectiva bibliotecă. (6)
Serverul are rolul de a asculta orice cerere de conectare, de creare a conexiunilor cu oricare
client și de a intercepta orice cerere din partea clienților, de a o valida și de a trimite mai departe
răspunsul către ceilalți clienți. Arhitectura serverului se bazează foarte mult pe sistemul de
21
gestionare a conexiunilor oferit de Socket.IO dar totul este gestionat de mediul Node.js. Astfel
serverul conține un singur modul și folosește mediul de interpretare oferit de motorul JavaScript
Google V8.
Robustețea acestui server vine din faptul că, spre deosebire de majoritatea serverelor care
trimit la intervale periodice informații către clienți, acesta este bazat pe evenimente, ceea ce face
ca informațiile să fie transmise doar când este nevoie. Acest lucru este foarte util pentru creșterea
perfomanței jocului, un aspect important pentru o aplicație destinată telefoanelor mobile. Având
în vedere faptul că jocul este bazat pe ture și doi jucători nu vor face cereri la server în același
moment, impactul asupra scăderii performanței va fi minim ceea ce face ca arhitecura bazată pe
evenimente să fie cea optimă.
Totodată serverul are rolul de a emite începerea jocului atunci când s-a acumulat numărul
necesar de clienți pentru o sesiune de joc și închiderea acesteia atunci când jocul a ajuns la
finalitate. Dat fiind folosirea unui server asincron Node.js comunicarea va fi facilitată prin
„callbacks”. Avantajul folosirii unui server cu operații non-blocante este faptul că un client nu se
va bloca așteptând anumite operații care ar putea dura o lungă perioadă de timp, astfel clientul se
bucură de toate funcționalitățile până când ajunge răspunsul. Acest aspect este important pentru
aplicații din punct de vedere al experienței utilizatorului dat fiind și faptul că jocul va rula pe o
mașină cu resurse limitate.
22
3.2 Modelarea datelor
În continuare voi prezenta în detaliu entitățile ce compun modulele descrise în subcapitolul
anterior, cât și relațiile dintre acestea.
3.2.1 Modelarea datelor pe aplicația client
Figura 7: Diagrama de clase a aplicației client
După cum se poate observa și în figura 8, am creat un obiect de bază GameManager care
va avea grijă ca tot ce se întamplă în joc și toate entitățile care vor fi create să fie gestionate
corect. Acest obiect este jocul propriu-zis care va avea grijă ca de fiecare dată jocul să se
comporte normal, să valideze toate input-urile utilizatorului, să configureze conexiunea la
internet și să redea pe ecran toate modificările.
În principal toată logica jocului depinde de două mari clase care sunt extinse pentru a oferi
funcționalități mai specifice. Acestea sunt GameObject și Abilities. Pentru buna organizare a
proiectului am creat și anumite clase ajutătoare care vor avea rolul ori de a gestiona anumite
funcționalități, ori de a oferi anumite informații relevante pentru desfășurarea jocului.
GameObject-ul descrie orice obiect grafic tridimensional aflat în scenă. Practic pentru
orice model tridimensional afișat, acesta va avea în spate un astfel de GameObject. De subliniat
aici faptul că aceste obiecte se referă strict la cele tridimensionale și nu la cele bidimensionale
cum ar fi butoanele interfeței grafice.
23
Pentru acest joc am extins această clasă abstractă în 3 noi clase pe care le-am considerat ca
fiind relevante: Square, Hero și Monster.
Pentru a explica motivul creării entității Square trebuie explicat mai întâi modul de gândire
al jocului în ceea ce privește harta. Jocul este unul bazat pe ture, astfel fiecare jucător va avea o
tură a lui în care va putea face anumite acțiuni. Toate aceste acțiuni trebuie să se întâmple într-un
spațiu de joc, o tablă. Asemenea unui joc de șah, tabla are mai multe diviziuni pentru a putea
organiza fiecare piesă de joc și defini anumite mutări permise. În cazul nostru cea mai mică
diviziune a tablei o semnifică Square care în plan grafic nu este nimic altceva decât un pătrat al
hărții. Totodată se poate observa că această entitate a fost extinsă iar în 3 clase pentru a oferi
funcționalități și mai specifice: NormalSquare, SpawnSquare, Wall. Aceste noțiuni vor fi
explicate mai în detaliu întru-un alt paragraf.
Hero este o entitate mobilă (prin mobilă mă refer la faptul că pe parcursul jocului această
entitate are posibilitatea de a fi mutată pe hartă) care este în mare parte gestionată de utilizator.
Aceasta reprezintă eroul jucătorului pe tablă. Fiecare jucător își poate personaliza eroul și astfel
modificările vor fi vizibile în joc.
Această entitate stă la baza jocului. Fiecare jucător va avea o singură astfel de entitate pe tot
parcursul jocului și va putea să o mute pe tabla de joc sau va putea folosi abilități ale acesteia.
Aceste acțiuni vor fi folositoare pentru îndeplinirea obiectivului fiecărui joc.
Monster este tot o entitate mobilă dar gestionată in totalitate de GameManager. Jucătorul
nu are acces la nici o funcționalitate a acesteia, reprezintând principalul obstacol în împlinirea
obiectivului de către jucători. Reprezintă entitatea anti-erou și se va deplasa pe harta de joc sau va
folosi abilități exact ca un erou. Deși nu este controlată de jucător, informațiile privind această
entitate pot fi inspectate folosind interfața grafică.
De menționat faptul că pentru a crea un joc unic de fiecare dată monștrii vor fi creați într-un
mod pseudo-aleatoriu. În schimb pentru a menține o doză de echilibru între eroi și inamici, la
generarea monstrului se va ține mereu cont de numărul de jucători prezenți și de specializarea lor.
În schimb entitatea care se va ocupa îndeosebi de crearea de Squares va fi Tile. Harta
acestui joc se va construi în timp ce eroii o vor explora. Pentru a evita construirea hărții prin
pătrate am adăugat o noua diviziune a hărții, de data aceasta una care cuprinde mai multe pătrate.
O grupare de astfel de entități va forma o placă (Tile). Un Tile este o entitate de 4x4 Squares.
24
Totodată această entitate este responsabilă de poziția pseudo-aleatorie a pătratelor ce o formează
și anumite funcționalități pentru a putea identifica cadrul său pe harta de joc.
După cum am menționat mai sus un Tile poate avea 3 tipuri de Squares. Cel mai ușor de
recunoscut este Wall (perete) care nu reprezintă nimic altceva decât obstacole în calea eroilor. Al
doilea tip este SpawnSquare (~pătrat de invocare) care reprezintă un pătrat special unde un
inamic își va face apariția pe hartă pentru prima dată. Cel de-al treilea este NormalSuqare care,
după cum îi spune și numele, nu are atribute speciale, este o divizie în plus pe harta de joc.
Abilities sunt entități fără o formă precisă, reprezentând abstractizarea modului de atac sau
reîmprospătare a eroilor. Sunt singura sursă de a elimina inamicii de pe tablă. Pentru a specializa
fiecare tip de abilitate, această clasă a fost extinsă în alte 3 clase: ActiveAbilities,
UtilityAbilities, PasiveAbilities.
ActiveAbilities sunt abilitățile care vor răni inamicii și pot avea diferite raze de acțiune în
funcție de specializarea eroului. Ele trebuie acționate de jucător. PasiveAbilities sunt abilitățile
care nu necesită acțiuni adiționale pentru a fi activate și își vor face efectul pe tot parcursul
jocului. UtilityAbilities sunt tipurile de abilități care vor afecta eroul care le activează și de
obicei vor reîmprospăta anumite atribute ale eroului.
MonsterAbilities este o clasă separată care reprezintă abilitățile folosite de către inamici.
Aceste entități sunt diferite prin modul logic de gândire față de cele ale eroilor. Totodată acestea
au incorporate și acțiunea de mișcare deoarece un inamic fiind controlat de către GameManager,
nu va fi mutat de către un input al utilizatorului.
PredefinedAbilities și PredefinedMonsterAbilities sunt două clase ajutătoare, câte una
pentru fiecare entitate mobilă, care vor popula posibilele abilități selectate pentru erou sau alese
pentru monstru. Acestea vor fi instanțiate doar o singură dată la încărcarea jocului.
AbilitiesManager este clasa care va gestiona orice abilitate folosită pe parcursul jocului,
fie că aceasta a fost acționată de un erou sau de un inamic. GameManager-ul îi va transmite că o
anumită enitate vrea să folosească o abilitate iar în funcție de sursa acțiunii, manager-ul va
acționa ca atare.
Pentru o abilitate folosită de un erou, AbilitiesManager va pregăti pentru execuție acea
abilitate prin clonarea ei. După caz acesta va activa sau dezactiva anumite părți ale interfeței
25
grafice. În primă fază va aștepta un input din partea utilizatorului, mai specific, o țintă pentru
abilitate. După ce un astfel de input a fost comunicat de GameManager, AbilitiesManager-ul va
valida selecția. Va verifica dacă obiectul selectat este o țintă validă pentru abilitate și dacă se află
în raza de acțiune a acesteia. În caz afirmativ îi va face cunoscut utilizatorului faptul că trebuie
apăsat butonul de aruncare zar. După aruncare se verifică dacă abilitatea va lovi ținta, se vor
calcula efectele și abilitatea va fi consumată.
Similar se întâmplă și pentru cazul în care un inamic invocă o abilitate. Diferența este faptul
că nu va aștepta input-ul utilizatorului și va face calculele automat. Totodată având în vedere că
pot fi mai mulți monștri care se vor activa la tura eroului, AbilitiesManager se va asigura că
abilitățile acestora se vor executa în ordine și într-un mod în care utilizatorul poate înțelege ce se
întâmplă.
Pathfinder este una dintre cele mai importante entități. Ea este responsabilă de calcularea
traiectoriilor, distanțelor și căilor atât pentru entitățile mobile cât și pentru abilități. Dacă
utilizatorul va dori să își deplaseze eroul, se va invoca Pathfinder-ul care va calcula cea mai
scurtă cale din punctul în care se află eroul, până în punctul unde se vrea a fi mișcat și va returna
această cale. Totodată această enitate mai este folosită pentru a calcula raza în care un erou se
poate mișca sau folosi o abilitate, sau raza de acțiune a unei abilități (în cazul în care eroul poate
ataca cu aceeași abilitate mai mulți monștri). Această cale sau zonă va fi mai apoi trimisă la
GameManager care se va ocupa de deplasarea obiectelor pe acea cale.
Alternativa folosirii acestei clase ar fi fost doar un validator care verifică poziția finală și
teleportarea obiectului în acea poziție. Dezavantajul este faptul că nu se fac verificări în privința
blocării drumului până în aceea poziție. Astfel eroul sau monstrul pur și simplu ar evita
obstacolele sau pereții hărții, ceea ce ar duce la un ritm de joc neinteresant. Fiind un joc
tridimensional și cu scopuri precise, mișcarea pe hartă aduce un plus de dinamism jocului.
GameCamera este entitatea responsabilă cu ce anume vedem pe ecran. Deși harta și
obiectele de pe ea sunt construite în memorie, pe ecran putem vedea doar o parte, astfel această
entitate, în funcție de mărimea ecranului, va afișa anumite cadre. Proprietățile camerei pot fi
modificate de către utilizator. Jucătorul poate să mute cadrul în oricare direcție a ecranului sau
poate mări sau micșora obiectele de pe el.
26
Construirea interfeței grafice este posibilă cu ajutorul clasei integrate în libGDX scene2d.
Deși această clasă oferă mult mai multe, deoarece jocul este tridimensional, a fost folosită doar
pentru construirea interfeței grafice. Aceasta conține doar elemente bidimensionale precum
butoane, imagini portret sau etichete.
Se poate observa faptul că toate elementele grafice de pe ecran sunt de diferite tipuri (atât
bidimensionale cât și tridimensionale). Din această cauză fiecare tip de obiect va avea diferite
moduri de interpretare a acțiunilor pe ecran, va avea un alt InputListener. Astfel pentru ca jocul
să interpreteze corespunzător fiecare acțiune a sa, am definit 3 astfel de InputListener, unul
pentru camera jocului, unul pentru obiectele tridimensionale și unul pentru interfață. Pentru ca
jocul să trateze toate cele 3 inputuri am folosit un multiplexor oferit de libGDX. Ordinea în
multiplexor a acestora este importantă deoarece este aceeași ordine în care evenimentele vor fi
verificate. Astfel pe primul plan avem interfața grafică, urmată de stratul obiectelor
tridimensionale și la final camera. Dacă una dintre acțiuni a fost consumată de interfață,
evenimentul nu va fi transmis în continuare și la celalalte InputListeners.
Pentru ca jocul să acționeze în timp real, libGDX va redesena fiecare cadru al ecranului de
60 de ori la fiecare secundă (acesta fiind modulul ideal, anumite resurse limitate ale dispozitivului
sau redesenarea unor obiecte care necesită un timp îndelungat pot micșora numărul de cadre
desenate). De aceea framework-ul ne pune la dispoziție metoda render(), metodă ce va fi apelată
de fiecare dată când jocul va redesena un cadru. În această metodă trebuie specificat ce obiecte
vor fi afișate pe ecran și anumite operații care trebuie făcute între cadre. În cazul de față
redesenăm la fiecare cadru toate obiectele tridimensionale, redesenăm scena pe care se află
interfața grafică, actualizăm camera jocului (care este indirect responsabilă de cadrele redate) și
tot aici verificăm dacă poziția obiectelor mobile trebuie actualizată.
Totodată în GameManager sunt configurate toate evenimentele care vor fi ulterior folosite
pentru comunicarea pe internet între joc si server.
27
3.2.2 Modelarea datelor pe server
Serverul are rolul de a negocia conexiunea cu clineții care vor să se conecteze și de a
facilita o bună desfășurare a jocului. Acesta este configurat pe platforma Node.js.
Serverul creat este un server HTTP cu ajutorul extensiilor oferite de Node.js și anume
express.js. După cum am specificat și în capitolul anterior acesta va folosi biblioteca de server a
platformei Socket.IO. Toate aceste configurări sunt ușor de realizat, motiv pentru care am ales
lucrul cu Node.js.
Serverul a fost setat să asculte la orice IP care folosește portul 8081. Folosind Socket.IO am
setat callback-urile pentru crearea unei conexiuni cu un client la fel și pentru deconectarea lui.
Totodată pentru fiecare conexiune realizată am setat evenimentele pentru care serverul o să
asculte pe acest socket.
var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);
var players = [];
var activePlayer;
server.listen(8081, function() {
console.log("Server is now running...");
});
Secțiunea de cod 1: Configurearea serverului HTTP
Serverul modelează în principal 2 entități: players și activePlayer.
Players reprezintă un vector de jucători care menține id-ul socketului și poziția fiecăruia.
La fiecare conectare a unui nou client, se va crea un nou obiect player și va fi adăugat în colecție.
Atunci când numărul de jucători a ajuns la 3 serverul va emite o cerere de începere a jocului la
fiecare client.
ActivePlayer este o entitate pe care o menține serverul pentru a ști a cărui jucător îi este
tura. Astfel de fiecare dată când un client va emite un eveniment prin care își exprimă încheierea
turii, acesta va ști care este următorul jucător la tură. Totodată serverul va emite startul turei la
clientul respectiv.
28
3.3 Interfața cu utilizatorul
Interfața cu utilizatorul este probabil cea mai importantă componentă a unui joc sau a unei
aplicații Android în general. Contează mult ca această interfață să fie intuitivă, să fie înțeleasă
foarte ușor de către utilizator și să îi vină în ajutor pentru a utiliza eficient aplicația.
Ca orice joc video am început prima interacțiune cu utilizatorul prin înfățișarea unui meniu
principal. Acest meniu conține patru butoane: Singleplayer, Multiplayer, Create Hero, Exit. În
spatele acestor butoane este plasată o imagine sugestivă.
Schema 1: Meniul principal
Acest meniu este cât se poate de clasic dar intuitiv. Butonul Singleplayer semnifică
începerea unui joc fără aliați, Multiplayer semnifică conectarea la internet și începerea unui joc
cu încă doi jucători aliați. Create Hero îl va duce într-un alt cadru unde va avea posibilitatea de
creare a unui nou erou iar Exit va scoate jucătorul din aplicația curentă.
Fiind un joc setat într-un cadru fantastic, acest lucru trebuie reflectat și în interfața
utilizatorului, de aceea am creat texturi pentru butoane care sunt specifice acestei teme. Imaginile
au fost realizate de mine cu ajutorul programului Adobe Photoshop.
29
Unul din cele mai complexe cadre este cel de construire a erolui reprezentat în schema[2].
Schema 2: Meniul de creare a unui erou
Centrat în partea de sus a paginii avem titlul acestei secțiuni. Pe primul rând din chenar
avem trei butoane, fiecare cu un portret sugestiv, care va semnifica speța eroului ce va fi creat. Pe
următorul rând avem specializările posibile, această specializare va influența abilitățile afișate în
următoarele rânduri. Meniul este făcut în așa fel încât jucătorul poate selecta o singură speță și o
singură specializare. În dreapta putem vedea proprietățile erolui. Acestea se vor modifica în
funcție de alegerile făcute în stânga. Poziționarea în dreapta a atributelor erolui este foarte
avantajoasă pentru utilizator deoarece va vedea aproape imediat ce avantaje și dezavantaje aduce
selecția lui. Totodată abilitățile au o zonă puțin separată pentru a fi inspectate pe larg de către
utilizator. În mijlocul părții de jos se poate observa un chenar pentru text. Acesta semnifică locul
unde se va scrie numele eroului. Astfel utilizatorul își poate personaliza fiecare erou cu un nume
original. În cazul în care utilizatorul este mulțumit cu eroul creat sau s-a răzgândit în privința
creării acestuia, are ca opțiune terminarea sau ieșirea din acest meniu, fiecare opțiune fiind
reprezentată de butonul Back și Done.
Înainte de începerea unui joc, fie el online sau nu, jocul îți va cere să selectezi un erou
pentru a-l folosi în jocul ce urmează.
30
Schema 3: Meniul de selecție a unui erou
Într-un chenar jocul va afișa cele 5 posibile alegeri pe care jucătorul le are. Fiecare erou are
identificată o casuță cu portretul speței și numele său sub ea. În momentul în care utilizatorul nu
are cinci eroi creați vor apărea doar cei care există.
În capătul de jos al ecranului sunt 3 butoane. Primul buton, ca și în ecranul prezentat
anterior, este pentru întoarcerea în meniul precedent în cazul în care utilizatorul se răzgândește să
înceapă jocul. Al doilea (cel din mijloc) este pentru a șterge eroul selectat. Ultimul buton
continuă cu începerea jocului în cazul în care utlizatorul s-a decis asupra unui erou.
Cea mai importantă interfață este în schimb cea în care utilizatorul a pornit un joc și toate
elementele grafice sunt afișate pe ecran.
31
Schema 4: Interfața în timpul unui joc
Utilizatorul trebuie să se poate bucura de toate funcționalitățile oferite de joc fară prea mult
efort. Astfel am divizat ecranul în 3 zone.
Prima zonă este cea din partea stângă a ecranului. Aici putem vedea o serie de butoane și un
chenar text. Acele butoane vor fi cele mai folosite în timpul jocului astfel le-am aranjat unele sub
altele într-o manieră logică. Primul buton este Move (a mișca) și va fi acțiunea principală a unui
erou. În acest joc, fiind un joc tactic, este foarte importantă poziționarea eroului pe hartă atât
pentru a evita inamici sau obstacole, cât și pentru a folosi abilități specifice. Al doilea buton este
Use ability (folosește abilitate). După poziționare este important să îți menții numărul de inamici
sub control astfel folosirea abilităților este recomandată. De aceea am așezat acest buton exact
sub butonul de mișcare. Al treilea buton este cel de încheiere a turei (End Turn). După ce eroul
și-a folosit toate acțiunile posibile de mișcare sau folosire a abilității, acesta poate semnala
încheierea turei lui. Desigur acest buton poate fi folosit oricând chiar și fără ca eroul să fi făcut o
acțiune. Ultimul buton este cel de Roll die (aruncare de zar). Acest zar este folosit la toate
abilitățile jocului pentru a verifica dacă abilitatea va fi executată cu succes. Deși este un buton
mult mai folosit decât cel de încheiere a turei, din punctul de vedere al unui utilizator, este mult
mai bine poziționat înafara acestor butoane. Ultima secțiune din stânga este reprezentată de un
chenar de tip text. Acest chenar are rolul de a afișa toate evenimentele ce se petrec în timpul
32
jocului. Vor fi afișate ultimele 3 acțiuni fie ale jucătorului, fie ale inamicilor sau a hazardelor.
Totodată acesta va avertiza încheierea unei ture și începerea alteia, precum și toate informațiile
legate de abilități sau zaruri aruncate. Am plasat acest chenar într-o zonă în care să nu încurce
spațiul de joc, dar în același timp să fie ușor de verificat sau urmărit.
A doua zonă este formată din cinci butoane mari și unul mai mic rotund în dreapta. Această
reprezentare urmează schema clasică a unui joc bazat pe rol, unde abilitățile sunt afișate în josul
ecranului sub formă de iconițe. Deoarece nu pe toate dispozitivele cu interacțiune touch avem
posibilitatea de a mișca un cursor fară a atinge ecranul (hover) am preferat ca pe butoane să fie
scris numele abilității ce vor fi folosite. Avem 5 butoane care desemnează cele 5 abilități alese în
timpul creării eroului. Butonul mic din dreapta semnifică butonul de pauză al jocului. În timp ce
pentru un joc fară internet acesta va pune pauză jocului, într-un joc de echipă acesta doar va afișa
un meniu de pauză dar fără a întrerupe jocul.
A treia zonă reprezintă informații referitoare la atributele eroului folosit de respectivul
utilizator și sunt afișate în partea de sus a ecranului. Iarăși aceasta urmărește schema unui joc de
rol. Astfel avem patru etichete în bara principală și încă două în colțul din dreapta exact sub bară.
În cea mai din stânga margine avem un portret reprezentând eroul. Am luat această decizie pentru
ca jucătorul, inafară de modelul afișat pe hartă, să mai folosească un punct de reper vizual.
Consider că acest detaliu aduce o foarte mare contribuție pozitivă din punctul de vedere al
experienței utilizatorului în aplicație. În continuare am ordonat cele patru etichete după
importanța lor în joc: punctele de viața ale eroului, armura sa, punctele de viteză pe hartă și
numărul de puncte de viață ale eroului atunci când va fi infrânt. Cele două etichete separate nu au
fost incluse în această bară deoarece, deși reprezintă entități care contribuie la erou, ele sunt
mereu resetate la fiecare joc și valorile lor sunt mereu modificate. Este o separare benefică pentru
utilizator.
După cum am precizat și în capitolele anterioare, o parte din interacțiunea cu jocul se va
realiza și cu obiectele tridimensionale din scenă dar interfața grafică va veni mereu în ajutorul
utilizatorului.
33
4. Implementare
Pentru acest capitol am ales să prezint o funcționalitate pe care am considerat-o dificilă și
complexă din punct de vedere a implementării. În acest sens am ales mișcarea obiectelor (și mai
specific a eroilor) pe hartă.
Mișcarea obiectelor pe hartă de către utilizator presupune 4 procese în cadrul jocului:
1. Selectarea obiectului care va fi mutat și selectarea obiectului destinație.
2. Calcularea ariei unde se poate mișca obiectul.
3. Calcularea căii între sursă și destinație.
4. Translatarea obiectului urmărind calea calculată.
Voi începe prin expunerea selectării unui obiect. În libGDX, spre deosebire de alte
framework-uri pentru dezvoltat jocuri, nu oferă eventListeners (ascultare de evenimente) pentru
obiecte tridimensionale, astfel că acest tip de eveniment trebuie simulat.
Pentru început am făcut ca GameManager-ul să asculte evenimentele ce vor avea loc pe tot
ecranul prin folosirea unui InputAdapter.
Prin aceasta a trebuit să implementez mai multe metode ce se vor apela la atingerea
ecranului. Cele mai importante sunt:
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button)
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button)
Prima metodă va fi apelată atunci când pe ecran este apăsat un punct, iar cea de-a doua va fi
apelată atunci când utilizatorul ridică degetul de pe ecran. În metoda touchDown am adaugat:
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
selectingGameObjectIndex = getObject(screenX, screenY);
return selectingGameObjectIndex >= 0;
}
34
Pentru a putea implementa o selecție naturală a obiectelor am definit două variabile de tip
întreg: selectingGameObjectIndex și selectedGameObjectIndex. Prima variabilă va marca
indexul obiectului selectat atunci când abia am apăsat pe ecran iar cea de-a doua va fi indexul
obiectului atunci când am ridicat degetul. Totodată vedem apelul unei metode noi getObject.
Corpul metodei arată astfel:
private Vector3 position = new Vector3();
private int getObject (float screenX, float screenY) {
cameraRay = mainCamera.getPickRay(screenX, screenY);
int result = -1;
float distance = -1;
for (int i = 0; i < instances.size; ++i) {
final GameObject instance = instances.get(i);
instance.transform.getTranslation(position);
position.add(instance.center);
float dist2 = cameraRay.origin.dst2(position);
if (distance >= 0f && dist2 > distance)
continue;
if (Intersector.intersectRaySphere(cameraRay, position, instance.radius, null)) {
result = i;
distance = dist2;
}
}
return result;
}
Metoda primește ca parametri poziția pe axa X și poziția pe axa Y a punctului care a fost
atins pe ecran. Mai întâi vom lua raza camerei din punctul în care a fost atins ecranul. Această
rază poate fi văzută ca o linie dreaptă care începe din camera jocului și se îndreaptă pe aceleași
coordonate X și Y spre spațiul Z [Figura 9].
Vom defini două variabile locale: result care va conține indexul celui mai apropiat obiect
găsit și distance care va fi distanța dintre obiect și originea acestuia. Toate obiectele
tridimensionale sunt ținute într-un vector global numit instances. Acest nume este datorat
faptului că, deși foarte multe obiecte folosesc același model, trebuie creată o instanță pentru
fiecare copie a acelui model în scenă. Astfel fiecare obiect este o instanță a acelui model.
Totodată am definit o variabilă globală position care va lua mereu coordonatele în spațiu ale
punctelor. Vom itera prin toate obiectele jocului și vom pune în position poziția fiecăruia.
35
Figura 8: Exemplul unei raze3
Pentru a lua centrul obiectului vom adăuga la vectorul de poziție a acestuia vectorul
centrului. În continuare calculăm distanța dintre obiect și originea razei din cameră și verificăm
dacă această distanță este mai mare decât cea precedentă. În caz afirmativ vom ignora acest
obiect și vom trece la următorul deoarece ne interesează cel mai apropiat obiect de cameră. În
continuare verificăm dacă obiectul se intersectează cu raza noastră. Acest lucru este posibil cu
ajutorul unei funcții linGDX, intersectRaySphere din clasa Intersector, care va calcula dacă
raza va intersecta marginile obiectului. În caz afirmativ vom lua indexul obiectului și vom
actualiza dinstanța minimă. În final dupa iterarea prin toate obiectele vom returna indexul celui
mai apropiat obiect.
Pentru a calcula limitele unui model avem nevoie de anumite operații. Pentru ușurință
aceste calcule au fost făcute în constructorul clasei GameObject.
public final Vector3 center = new Vector3();
public final Vector3 dimensions = new Vector3();
private final static BoundingBox bounds = new BoundingBox();
public GameObject (Model model, float x, float y, float z, String id) {
super(model, x, y, z);
this.id = id;
calculateBoundingBox(bounds);
bounds.getCenter(center);
bounds.getDimensions(dimensions);
radius = dimensions.len() / 2f;
}
3Sursa pozei https://www.codeproject.com/Articles/35139/Interactive-Techniques-in-Three-dimensional-Scenes
36
Din fericire libGDX conține o clasă ajutătoare BoundingBox care ne va ajuta la aceste
calcule. După creareea obiectului vom apela metoda calculateBoundingBox (această metodă
este definită în clasa ModelInstance pe care GameObject o extinde) având ca parametru de ieșire
variabila bounds. Acum am aflat toate informațiile despre marginile obiectului. Deoarece centrul
limitelor nu coincide cu originea modelului, vom pune această valoare în variabila center. Totuși
am optat ca aceste limite să nu fie cât obiectul în sine așa că formăm o rază egală cu jumătate din
limitele obiectului.
Revenind la metoda touchDown vedem că, dacă la apăsare, a fost selectat un astfel de
obiect, metoda va returna true și va atribui indexul lui variabilei selectingGameObjectIndex, în
caz contrar, va returna false.
Utilizatorul poate mișca degetul pe ecran fără să îl ridice o bună perioadă de timp, de
aceea m-am asigurat că odată ce el va ridica degetul de pe ecran, obiectul selectat la atingerea
ecranului coincide cu cel care este selectat atunci când a ridicat degetul. Astfel în metoda
touchUp avem:
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
if (selectingGameObjectIndex >= 0) {
if (selectingGameObjectIndex == getObject(screenX, screenY)) {
resolveTouchObject();
}
selectingGameObjectIndex = -1;
return true;
}
return false;
}
Dacă aceste două obiecte coincid vom rezolva posibilele rezultate în metoda
resolveTouchObject. Dacă selectăm pentru prima dată un obiect, se va apela metoda setSelected
având ca parametru selectingGameObjectIndex.
private void setSelected (int value) {
if (selectedGameObjectIndex == value) {
selectedGameObjectIndex = -1;
return;
}
selectedGameObjectIndex = value;
}
37
Prima dată verificăm dacă există un obiect selectat și acesta coincide cu noua selecție. În
cazul selectării primului obiect acest test va fi evitat. Dacă ar fi fost pozitiv atunci obiectul ar fi
fost deselectat . După setăm această selecție ca obiectul selectat.
Dacă după prima selecție (în cazul în care am selectat mai întâi un erou) dacă urmărim din
nou firul aplicației vom ajunge iar în resolveTouchObject(). De data aceasta avem o altă
verificare.
if (selectedGameObjectIndex != -1 &&
instances.get(selectedGameObjectIndex) == myHero &&
(instances.get(selectingGameObjectIndex).getGameObjectType().equals("NormalSquare") ||
instances.get(selectingGameObjectIndex).getGameObjectType().equals("SpawnSquare")) &&
lastAction == Action.MOVE) {
resolveMoveTouch();
}
În cazul acesta verificăm dacă eroul selectat este eroul utilizatorului, dacă butonul de
mișcare este apăsat și dacă obiectul următor selectat este unul valid pentru a se deplasa (dacă este
un pătrat normal sau unul de invocare, validarea însă dacă aceste pătrate sunt ocupate se va face
mai târziu). În caz afirmativ se va apela metoda resolveMoveTouch.
În continuare voi prezenta calcularea căii de la erou la punctul selectat ca destinație. Voi
omite prezentarea calculării ariei spațiului unde eroul se poate deplasa deoarece aceasta este
similară. Acestea vor folosi o clasă special implementată numită Pathfinder (~căutător de căi).
Pentru aceste două funcționalități a fost nevoie de implementarea a doi algoritmi (unul fiind
inclus în logica celui de-al doilea): Algoritmul lui Dijkstra și A* (citit A star). Am ales acești
algoritmi nu numai pentru faptul că sunt doi algoritmi cunoscuți, ci și pentru faptul că sunt doi
algoritmi optimi și robuști care garantează performanță mare, memorie redusă de folosire și un
rezultat corect. Astfel în continuare voi prezenta câteva noțiuni despre acești algoritmi.
Algoritmul lui Dijkstra este un algoritm folosit pentru a găsi cea mai scurtă cale între
două noduri într-un graf. Principiul de funcționare a algoritmului este simplu. Se începe cu nodul
sursă și se setează ca nodul curent. Restul nodurilor din graf sunt marcate ca nevizitate. Se ia
fiecare nod adiacent nodului curent și se calculează pentru fiecare un cost pentru deplasare.
Costul de deplasare este întotdeauna calculat ca și costul nodului precedent adunat cu costul
38
mutării pe nod și este alipit acestuia. După ce toți vecinii au fost verificați, marcăm nodul ca fiind
vizitat. Dacă nodul destinație a fost marcat ca vizitat, oprim algoritmul și vom returna distanța
până la nodul sursă. Daca toate nodurile posibile au fost vizitate dar nu s-a ajuns la nodul
destinație atunci algorimul se va opri și va returna infinit pentru că nu există o cale de la sursă la
destinație. Dacă nu este nici unul din cazurile precedente, se caută nodul cu costul cel mai mic, se
setează ca și nod curent și se continuă algoritmul cu vizitarea vecinilor în buclă până se ajunge la
una din condițiile de oprire. (8)
A* este un algoritm care urmează aproximativ aceeași pași ca și cel a lui Dijkstra. Ceea ce
diferă între acești algoritmi este faptul că la calcularea costului unui nod se va lua în considerare
o euristică care va determina dacă nodul curent se îndreaptă spre nodul destinație sau se
îndepărtează, astfel micșorând mult spațiul de căutare. O altă diferență ar fi că fiecare nod va
avea o referință a părintelui său, astfel că odată ajuns la nodul destinație, pentru returnarea căii, se
va itera prin părinții nodurilor de la țintă la destinație.
Revenind la aplicație, trebuie menționat faptul că odată ce jucătorul a apăsat butonul de
deplasare Pathfinder-ul va calcula spațiul în care eroul se va putea mișca. Având în vedere că
acea funcționalitate este realizată prin apelarea aproximativ a acelorași metode, voi omite
prezentarea acesteia. Diferența este că nu se va seta un nod destinație și tot din acest motiv nu se
va seta nici o euristică pentru a fi calculată la costul deplasării (ar fi rendundant și imposibil).
Corpul metodei resolveMoveTouch cuprinde:
public Pathfinder pathfinder = new Pathfinder();
public Array<Square> highlightedSquares = new Array<Square>();
private void resolveMoveTouch()
{
if(highlightedSquares.contains((Square)
instances.get(selectingGameObjectIndex),false)) {
path = pathfinder.findPath(instances, myHero.heroesPositionSquare,(Square)
instances.get(selectingGameObjectIndex));
///
}
}
39
Pentru ușurință am folosit pătratele hărții ca fiind nodurile grafului folosit de către cei doi
algoritmi prezentați mai sus. Variabila highligthedSquares conține toate pătratele posibile pe
care eroul curent își poate muta poziția. Astfel întâi verific dacă poziția selectată de utilizator
face parte din pozițiile disponibile pentru acesta. În caz afirmativ voi invoca Pathfinder-ul cu
metoda findPath și voi determina cea mai scurtă cale între pătratul eroului
(myHero.heroesPositionSquare) și pătratul selectat ca și pătrat destinație
(instances.get(selectingGameObjectIndex)).
Primul lucru pe care l-am făcut în această metodă a fost reîmprospătarea instanțelor
jocului (în cazul în care jocului i-au fost adăugate noi plăci pe hartă sau eroii sau inamicii s-au
deplasat), setarea pătratului destinație și inițializarea primului nod din graf ca fiind pătratul sursă.
Am creat o nouă listă notVisited care va fi populată cu nodurile ce urmează a fi verificate.
pathInstances = instances;
this.targetSquare = targetSquare;
notVisited.add(new Node(sourceSquare,null));
node = notVisited.get(0);
Se poate observa folosirea unei noi clase și anume clasa Node:
public class Node {
final float directCost = 1f;
public Node parent;
public Square squareObject;
public float F;
public float G;
public float H;
public Node(Square squareObject, Node node) {
this.squareObject = squareObject;
this.parent = node;
}
}
Primul atribut este o constantă directCost semnificând costul unei mutări urmată de
atributul parent specificând nodul părinte și squareObject semnificând pătratul la care face
referință acest nod. Cele trei variabile F, G și H sunt folosite pentru a calcula costul nodului.
Acestea sunt notații standardizate pentru algoritmul A*.
40
În continuare, în corpul metodei findPath am construit algoritmul A*:
while(notVisited.size > 0) {
float minF = notVisited.get(0).F;
for (Node node : notVisited)
if (node.F <= minF) {
minF = node.F;
this.minNode = node;
}
if(this.minNode.squareObject.equals(targetSquare)) {
while (!minNode.squareObject.equals(sourceSquare)) {
Path.add(new Vector2(minNode.position.x, minNode.position.y));
if (minNode.parent == null) {
Path.add(new Vector2(sourceSquare.xPosition,
sourceSquare.yPosition));
break;
}
minNode = minNode.parent;
}
Path.reverse();
return Path;
}
checkAndSetSquareNeighbours(minNode, 0);
notVisited.removeValue(minNode, true);
visited.add(minNode);
}
În testul buclei while se verifică daca am trecut prin toate nodurile din spațiul de căutare,
în caz negativ bluca va căuta în continuare o cale. De specificat faptul că acest test va deveni
pozitiv doar atunci când nu se poate ajunge la nodul destinație. Pentru a nu întâmpina anomalii
sau excepții vom inițializa costul minim cu costul primului nod din colecția de noduri nevizitate.
Calculul costului nodului se calculeaza apelând metoda calculateCosts a clasei Node.
public void calculateCosts(GameObject destination, boolean isDiagonal) {
this.H = calculateHeuristic();
if (parent != null)
this.G = parent.G + directCost;
else
this.G = directCost;
this.F = this.G + this.H;
};
În expresia costului am introdus o euristică determinată de distanța euclidiană între nodul
curent și nodul destinație calculată si returnată de metoda calculateHeuristic . După aceasta se
va calcula costul final si se va stoca în variabila F.
41
În continuare iterez prin toate nodurile nevizitate și găsesc nodul cu costul minim. Dacă
nodul selectat cu costul minim este nodul destinație, atunci pornind de la acest nod iterăm printre
părinții fiecărui nod până ajungem la nodul sursă. Acest lucru este marcat prin faptul că nodul
sursă nu are părinte. La fiecare iterație adăugăm nodul vectorului Path care va returna calea.
Când este găsit nodul sursă, bucla este oprită, se va inversa calea (deoarece aceasta este construită
de la destinație la sursă iar ea va fi folosită vice-versa) și se va returna.
Dacă nodul curent nu este cel final, se va căuta calea în continuare în metoda
checkAndSetSquareNeighbours:
private void checkAndSetSquareNeighbours(Node node, int maximumRange) {
for (GameObject objectInstance : pathInstances)
if (!objectInstance.getGameObjectType().equals("Hero") &&
!objectInstance.getGameObjectType().equals("Monster") &&
!objectInstance.getGameObjectType().equals("Wall"))
if (maximumRange > 0)
//această metodă este folosită pentru a calcula aria de mișcare
setSquareNeighbours(objectInstance, node, maximumRange);
else
setSquareNeighbours(objectInstance, node, 0);
}
Iterăm prin toate obiectele jocului eliminând cele care nu sunt pătrate. Pentru fiecare vom
apela metoda setSquareNeighbours în care verificăm dacă poziția lui coincide cu una din
pozițiile adiacente nodului curent:
if (x != node.squareObject.xPosition || y != node.squareObject.yPosition) {
if (x == objectInstance.transform.getTranslation(position).x &&
y == objectInstance.transform.getTranslation(position).y) {
this.node = new Node((Square) objectInstance, node);
if (!checkCollectionsForExistingItems(visited) &&
!checkCollectionsForExistingItems(notVisited)) {
if (x == y || (x + y) == 2)
this.node.calculateCosts(targetSquare, true);
else
this.node.calculateCosts(targetSquare, false);
notVisited.add(this.node);
}
}
}
42
În caz pozitiv creăm un nod nou și verificăm dacă cumva acest nod a fost deja iterat și pus
în una din colecțiile visited (pentru nodurile deja vizitate) și notVisited (pentru nodurile încă
nevizitate dar care urmează a fi). Dacă nu este prezent, îi calculez costul și îl voi adăuga în
colecția notVisited pentru a fi testat ulterior.
Trebuie specificat faptul că în jocul prezent costul mișcării pe diagonală este egal cu
costul mișcării directe. Totuși am lăsat loc extinderii acestei funcționalități.
În momentul de față PathFinder-ul este capabil să calculeze o cale de la punctul sursă
până la cel destinație și să returneze poziția fiecărui punct din acea cale. Revenim la corpul
metodei resolveMoveTouch unde în variabila path avem calea care trebuie urmată. Totuși acest
lucru nu este de ajuns pentru a pune obiectul în mișcare.
private void resolveMoveTouch()
{
if(highlightedSquares.contains((Square)
instances.get(selectingGameObjectIndex),false)) {
///
myHero.setPath(path);
myHero.hasMoved = true;
myHero.setFinalPosition(path.get(0).x, path.get(0).y);
}
}
În libGDX și în majoritatea motoarelor de dezvoltare a jocurilor pentru a mișca obiectele
pe scene ele trebuie translatate manual, acest lucru însemnând că fiecare obiect ce se vrea a fi
mișcat trebuie actualizat pe fiecare cadru desenat.
Pentru a realiza acest lucru am creat metode speciale pentru obiecte și am adăugat o
variabilă pentru a semnala faptul că obiectul este în mișcare. Astfel am în clasa GameObject:
public boolean hasMoved= false;
public void setPath(Array<Vector2> path){
this.Path = path;
}
public void setFinalPosition(float finalX, float finalY) {
this.finalPositionX = finalX;
this.finalPositionY = finalY;
}
43
În metoda setPath adaug calea tocmai calculată pentru a semnala faptul că obiectul
trebuie să o urmeze. În setFinalPosition setez întotdeauna următorul punct pentru a fi urmat din
cale. Inițial setez ca punctul la care trebuie ajuns este primul punct din path și setez variabila
booleană pozitiv pentru a marca faptul că acest obiect trebuie mișcat. Până în acest punct am
pregătit obiectul pentru translație.
În acest moment trebuie calculat la fiecare cadru redat pe ecran cu cât trebuie mișcat
obiectul și în ce direcție. Pentru realizarea acestui lucru am implementat o nouă metodă:
private Vector2 velocity = new Vector2();
private float tolerance = 0.1f;
private float speed = 10f;
public void updatePosition(float deltaTime) {
float angle = (float) Math.atan2(this.finalPositionY -
this.transform.getTranslation(position).y,
this.finalPositionX -
this.transform.getTranslation(position).x);
velocity.set((float)Math.cos(angle) * speed, (float) Math.sin(angle) * speed);
this.transform.translate(velocity.x * deltaTime, velocity.y * deltaTime, 0f);
isNodeReached();
}
Prima dată calculez unghiul în care obiectul trebuie să se deplaseze, astfel în variabila
angle stochez cotangenta diferenței valorilor punctului destinație și a punctului curent. În
variabila velocity setez puterea mișcării obiectului. Acest lucrul îl realizez prin adăugarea unei
viteze fiecărei coordonate a punctului. Aplic cosinus pentru coordonata x și sinus pentru
coordonata y pentru a afla unde anume trebuie poziționat obiectul. Deoarece pe anumite
dizpozitive există riscul ca jocul să prindă momente de latență, această mișcare trebuie filtrată cu
deltaTime. Această variabilă reprezintă timpul scurs între două cadre desenate, astfel deși poate
pe un dizpozitiv merge mai lent jocul, obiectele vor porni și vor ajunge în același timp ca pe un
dispozitiv cu resurse mai mari. În final aplicăm aceste transformări obiectului.
După cum am specificat această operație trebuie realizată la fiecare cadru desenat astfel că
în metoda render a GameManager-ului am scris următoarele linii de cod:
44
for(HashMap.Entry<String,Hero> entry : heroes.entrySet()) {
if(entry.getValue().hasMoved) {
entry.getValue().updatePosition(Gdx.graphics.getDeltaTime());
}
}
Astfel pentru fiecare obiect care trebuie mișcat aplicăm aceste transformări. În acest
moment obiectul va urmări primul punct din cale urmărind o mișcare rectilinie uniformă dar nu
se va îndrepta spre celelalte deoarece punctul reper nu este resetat. De aceea în updatePosition
avem un apel către metoda isNodeReached.
public void isNodeReached() {
if ((Math.abs(this.transform.getTranslation(position).x –
this.finalPositionX) <= tolerance) &&
(Math.abs(this.transform.getTranslation(position).y –
this.finalPositionY) <= tolerance)) {
this.transform.setTranslation(this.finalPositionX,this.finalPositionY, 2f);
if (Path.size >= 2) {
this.setFinalPosition(Path.get(1).x, Path.get(1).y);
this.Path.removeIndex(0);
} else {
this.finalPositionX = 0;
this.finalPositionY = 0;
this.hasMoved = false;
}
}
}
La fiecare apel trebuie verificat dacă obiectul a ajuns la destinația setată. Deoarece
variabilele raționale nu ne garantează faptul că obiectul va ajunge exact în punctul destinație, am
facut un test adițional în care verific dacă poziția obiectului în mișcare este apropiat de cel
destinație la o anumită toleranță. Această toleranța trebuie bine definită astfel că dacă valoarea
este prea mică va face ca ea să fie ignorată în timp ce dacă este prea mare aceasta va face ca
obiectul să se teleporteze la destinație. Dacă se intră în intervalul acestei toleranțe vor fi tratate
două cazuri. Dacă mai există puncte în cale, noua poziție finală va fi urmatorul nod din aceasta,
altfel înseamnă că am ajuns la punctul final, resetăm poziția finală și marcăm obiectul ca fiind
staționar. În ambele cazuri totuși setăm poziția obiectului exact în poziția finală pentru a nu avea
deviații în mișcarea obiectului. Acest lucru ne este garantat de faptul că punctele căii sunt numere
întregi. În acest moment obiectele se pot mișca pe suprafață de joc în timp real.
45
5. Manual de utilizare
5.2 Meniul principal
Prima interacțiune pe care o va avea utilizatorul aplicației este meniul principial. Acesta
este punctul de reper pentru toate celelalte funcționalități. Meniul conține patru butoane, fiecare
cu un mesaj sugestiv în engleză:
1. Butonul de joc „Singleplayer” semnifică începerea unui joc cu un singur jucător;
2. Butonul de joc „Multiplayer” semnifică începerea unui joc în mai mulți jucători;
3. Butonul „Create a hero” semnifică creearea unui erou pentru desfășurarea jocului;
4. Butonul „Exit”, unde utilizatorul poate părăsi aplicația.
Imaginea 1: Meniul principal
Desigur, prima intenție a unui jucător va fi de a apăsa butonul pentru un joc cu un singur
jucător pentru a-l testa. În acest moment va fi afișat un chenar simplu și gol deoarece utilizatorul
nu a creat nici un erou. Din fericire acest meniu conține două butoane: butonul de a adăuga un
nou erou sau de a șterge unul deja creeat. În acest caz vom folosi butonul de creare marcat cu
simbolul „+”.
46
5.3 Meniul de creare a unui erou
Acest meniu este unul complex, conținând informații despre rolul eroului în jocurile ce
vor urma. Meniul este împărțit în mai multe secțiuni.
Pe primul rând se află posibilele tipuri de ființe pe care jucătorul le poate alege. Fiind un
joc fantastic aceste ființe au diverse caracteristici diferite de ale oamenilor. Utilizatorul are 3
opțiuni: Star-elves, Moon-elves, Ferrevum.
Pe al doilea rând se află specializările pe care eroul le poate avea. Utilizatorul are la
dispoziție 3 specializări: Ranger, Warrior, Wizard. Specializarea va influența decisiv rolul erolui
în joc deoarece aceasta va defini ce tipuri de abilități va folosi în joc. De exemplu dacă
specializarea eroului este „wizard” (vrăjitor) acesta va folosi abilități magice care vor fi invocate
de la distanță, în timp ce pentru un „warrior” (războinic) abilitățile acestuia pot fi folosite
împotriva monștrilor adiacenți.
Aceste două selecții de mai sus sunt obligatorii și se poate alege o singură opțiune din
fiecare. Totodată acestea vor influența și atributele erolui. Acestea sunt afișate în partea de sus
dreapta a meniului sub titlul „stats” (atribute).
Imaginea 2: Meniul de creare a unui erou
47
Eroul are patru atribute: health (puncte de viață), armor (armură), speed (viteză), surge
(reîmporspătare). Fiecare dintre acestea va caracteriza eroul în joc astfel:
1. Punctele de viață semnifică câte puncte de vătămare va putea primi până acesta va
fi înfrânt.
2. Punctele de armură sunt punctele care semnifică cât de rezistent este eroul în
luptă. Dacă valoarea atacului depășește această valoare, armura este pătrunsă iar
eroul va fi rănit, în caz contrar armura rezistă iar eroul nu va suferi nici un efect.
3. Viteza eroului semnifică câte pătrate se va putea mișca eroul pe hartă.
4. Punctele de reîmprospătare semnifică câte puncte de viață va regenera eroul după
ce va fi înfrânt.
Sub acestea sunt două mari secțiuni conținând 10 abilități (Abilities). Utilizatorul trebuie
să aleagă 5 abilități din cele afișate. O dată ce o abilitate este selectată, un meniu va apărea cu
numele, efectele, numărul de atac și numărul de puncte de vătămare. Numărul de atac semnifică
numărul adunat la valoarea zarului de atac. Dacă aceste două valori adunate sunt egale sau mai
mari decât valoarea armurii inamicului, atacul se consideră a fi cu succes. Dacă atacul este cu
succes, inamicului i se retrag din punctele de viață, punctele de vătămare ale abilității.
Pentru a identifica eroul, acestuia i se va atribui un nume. Acest nume poate fi introdus
sub chenarul principal al meniului.
În stânga numelui va fi butonul „back” (înapoi) unde utilizatorul se poate întoarce în
meniul precedent iar în dreapta acestuia este butonul „done” (terminat) unde utilizatorul poate
semnala faptul că este mulțumit cu opțiunile alese și dorește crearea eroului. Eroul nu va fi creat
daca utilizatorul nu a introdus un nume sau nu a selectat exact cinci abilități. Dacă eroul este creat
va apărea pe ecran un mesaj de confirmare.
Acum dacă revenim în meniul de alegere a erolui vom vedea eroii creați. Aceștea pot fi
identificați după nume dar și după poza sugestivă reprezintând tipul eroului. Pentru a reveni la
meniul precedent utilizatorul poate folosi butonul „back”. Pentru a începe jocul jucătorul poate
apăsa butonul de start (start).
48
5.4 Interfața utilizatorului
Interfața cu utilizatorul conține toate informațiile necesare unui jucător pentru a-și
desfășura sesiunea de joc. Astfel aceasta a fost structurată pentru ca interacțiunea să fie optimă și
intuitivă.
Imaginea 3: Interfața utilizatorului și afișarea obiectivului
În partea de sus sunt afișate caracteristicile eroului. Pe parcursul jocului aceste valori se
vor modifica, astfel jucătorul va fi anunțat în timp real de orice modificare. În stânga acestor
valori este afișat un portret cu eroul respectiv. În dreapta sunt afișate alte două valori: experience
(experiență) și surges („reîmprospătări”). De-a lungul jocului eroul va înfrunta inamici, odată ce
acești inamici vor fi eliminați fiecare va genera puncte de experiență. Experiența este stocată la
nivel de joc si nu de erou, astfel într-un joc cu mai mulți jucători toți vor contribui la această
valoare. Experiența este utilizată pentru a evita diferite hazarde explicate mai jos. Numărul de
reîmprospătări semnifică de câte ori poate reveni în joc un erou după ce este înfrânt. La fel ca și
experiența, acest număr este la nivel de joc si oricare joc va începe cu valoarea 2.
În partea stângă sunt butoanele pentru diferitele acțiuni ale jucătorului: mutarea erolui,
folosirea unei abilități, încheierea turei și aruncarea zarului. Sub aceste butoane avem un spațiu
rezervat pentru afișarea diferitelor acțiuni ale jocului. Astfel în acest spațiu se pot consulta
49
ultimele trei evenimente ale jocului și este constant împrospătat. Aici se pot consulta valorile
zarurilor, acțiunile eroilor sau ale inamicilor, efectele hazardelor și altele.
În partea de jos avem cinci butoane, unul pentru fiecare abilitate a erolui. Fiecare buton
conține numele abilității care va fi invocată. După aceste butoane avem un buton rotund pentru
meniul de pauză. Acesta va înterupe jocul curent și se va afișa un meniu cu două opțiuni:
părăsirea jocului curent sau revenirea la acesta.
Diferite părți ale interfeței vor fi indisponibile în anumite momente ale jocului. Acest
lucru este semnalat prin colorarea gri a butoanelor.
5.5 Desfășurarea unui joc
Scopul jocului este de a duce la bun sfârșit obiectivul setat la începutul acestuia.
Obiectivul va fi ales la întâmplare la fiecare start de joc. Un meniu va fi afișat exact la începutul
sesiunii și va conține o descriere cu ce acțiuni trebuie făcute. Acestea pot varia de la eliminarea
unui lider până la distrugerea unei trupe de monștri.
În continuare voi explica anumiți termeni specifici jocului pentru o mai bine înțelegere a
desfășurării acestuia:
Pătrat, este unitatea de măsurare a spațiilor de pe hartă. Acestea pot fi pătrate
normale, ziduri sau pătrate de invocare a inamicilor.
Placă, este o colecție de 16 pătrate care conține diferite tipuri de pătrate.
Hazard, un eveniment aleatoriu în timpul jocului emis la fiecare sfârșit de tură.
Fiind un joc bazat pe ture fiecare jucător va avea tura lui. Fiecare tură se desfășoară în
aceeași ordine astfel:
1. Acțiunile erolui.
2. Explorarea în continuare a peșterii.
3. Rezolvarea unui hazard.
4. Acțiunile inamicilor.
Jocul pornește cu doar două plăci explorate urmând ca eroii să descopere la fiecare tură
noi părți din această peșteră pentru a găsi calea spre împlinirea obiectivului.
50
Eroul curent poate face două acțiuni. Acțiunile disponibile sunt mutarea eroului sau
folosirea unei abilități. Eroul poate folosi doar o singură acțiune de atac (folosirea unei abilități)
și două de mișcare la fiecare tură (unii eroi nu se vor supune cu exactitate acestor reguli).
Imaginea 4: Marcarea pătratelor unde se poate muta eroul
Pentru ca utilizatorul să își mute caracterul acesta va trebui să apese pe butonul de
„move”. Odată apăsat, toate pătratele unde eroul poate ajunge vor fi marcate cu verde. Aria
acestei zone este determinată de viteza eroului, astfel la o viteză de 2, eroul se va putea mișca 2
pătrate în orice direcție. Se poate mișca și pe diagonală. Dupa aceasta jucătorul va selecta eroul
său dupa care va apăsa pe pătratul unde dorește ca eroul să fie mutat. Dupa ce este mulțumit de
poziție, va apăsa pe același buton dar care are un mesaj diferit „end move” (încheiere mișcare).
Acest lucru va consuma o acțiune.
Pentru a ataca, eroul trebuie să apese pe butonul de folosire a abilităților, după care va
selecta o abilitate din cele 5 afișate. După selecție îi este indicat jucătorului să aleagă o țintă
pentru abilitate. Dacă ținta nu este validă va fi afișat un mesaj de atenționare, în caz contrat este
selectată ținta și utilizatorului îi este indicat să arunce zarul. După ce este apăsat butonul de zar se
va verifica dacă abilitatea este folosită cu succes sau nu. Aceste informații pot fi consultate în
locul rezervat evenimentelor marcate cu „log”.
51
Succesului unei abilități este calculat astfel: se ia valoarea zarului aruncat, se adună cu
valoarea atacului abilității și se compară cu armura inamicului. Dacă această comparație este mai
mare sau egală se consideră ca abilitatea fiind folosită cu succes. În acest caz se va extrage din
punctele de viață ale țintei numărul de puncte de vătămare.
În cazul în care inamicul are 0 sau mai puține puncte de viață acesta este considerat
distrus, va fi scos de pe hartă și va genera puncte de experiență pentru eroi.
Imaginea 5: Folosirea unei abilități și afișarea informațiilor în Log
Calcularea abilităților este identică pentru inamici iar valoarea zarului este aleasă aleator
și automat de către joc. Fiecare abilitate folosită cu succes de către eroi, monștri sau hazarde vor
fi semnalate prin diferite efecte vizuale precum flăcari sau raze de lumină.
Dacă eroul nu mai are acțiuni, acesta va trebui să apese pe butonul de încheiere a turei
(„end turn”) lucru sugerat și de către joc. După aceasta se va continua tura în modul specificat
mai sus.
În cazul în care un erou și-a încheiat tura pe un pătrat la marginea unei plăci și nu este zid,
acesta va explora o placă nou generată. Fiecare placă va avea un pătrat diferit pentru a simboliza
pătratul unde inamicul își va face apariția. La fiecare placă nou generată se va genera și un nou
inamic. Inamicii vor acționa întotdeauna la tura acelui erou la care au fost invocați.
52
Inamicii vor fi generați în funcție de viața eroilor, de numărul de jucători și tipul eroilor.
Astfel într-un joc cu un singur erou moon-elf, monștrii vor avea puncte de viață puține, vor
genera puține puncte de experiență și vor avea o probabilitate mai are să fie de tipul opus eroului.
Fiecare monstru are la rândul lui un nume, un tip și o serie de abilități. Abilitățile se vor efectua
în cascadă, astfel dacă prima abilitate nu poate fi folosită o va încerca pe următoarea. Această
căutare se oprește atunci când o abilitate poate fi utilizată sau când nu mai sunt abilități de căutat,
caz în care inamicul nu face nici o acțiune.
Toate aceste informații pot fi consultate atunci când utilizatorul apasă pe un inamic.
Imaginea 6: Caracteristicile unui inamic
După explorare anumite evenimente se vor întâmpla în această peșteră. Un meniu va fi
afișat unde este prezentată o descriere a ceea ce va urma să se întâmple și încă două butoane, unul
pentru a folosi experiența acumulată pentru a evita hazardul și altul pentru continuarea jocului cu
evenimentele descrise. Orice hazard poate fi evitat folosind exact 5 puncte de experiență. Datorită
dificultății, jumătate dintre aceste hazarde au posibilitatea de a fi benefice eroului. Spre exemplu,
un hazard negativ poate fi reducerea armurii unui erou cu 2 pentru următoarele două ture în timp
ce unul pozitiv poate fi regenerarea a două puncte de viață.
53
După hazard, dacă eroul activ a invocat inamici în această rundă sau runde precedente,
aceștia vor acțiuna în funcție de abilitățile lor. Toate aceste acțiuni vor fi regăsite în spațiul
dedicat evenimentelor. Monștrii acționează în ordinea invocării lor pe hartă.
După toate aceste evenimente se va semnala încheierea turei curente și jocul va continua
cu tura următorului jucător. În cazul unui joc cu un singur jucător, va urma tot tura acelui jucător.
Figură 1: Începerea unei noi turi
54
6. Concluzii
Scopul acestei aplicații este de a oferi un mod de joc mult mai dinamic din punctul de
vedere al jucătorului și de a testa modul în care acesta reacționează la schimbări. Acestea au fost
realizate cu ajutorul anumitor elemente precum generatea pseudo-aleatorie a hărții de joc și
generarea aleatorie a inamicilor. Toate aceasta contribuie la originalitatea jocului care dorește să
nu plictisească utilizatorul. Totodată aplicația a fost dezvoltată pentru telefoane, oferind
posiblitatea de a fi jucată oriunde și într-o perioadă relativ scurtă de timp. Deoarece aplicația este
destinată pasionaților de jocuri, aceasta este plasată într-un cadru fantastic având elemente
specifice acestui gen: creaturi fantastice și magie.
Deși jocul în sine nu explorează o gamă largă de evenimente și interacțiuni între joc și
utilizator, datorită stilului de joc acesta poate fi îmbunatățit. În acest sens jocului i se pot adăuga
rase noi de eroi, noi specliatități cu abilități specifice, evenimente noi, obiective noi și chiar
suprafață mai mare de joc. Totodată adăugarea de animații personajelor de pe tablă ar putea
aduce un foarte mare plus atmosferei jocului, aducând lucrurile mai aproape de realitate. În
ultimul rând, aplicației i s-ar putea aduce un sistem de răsplată mult mai potent. Căutarea de
artefacte și obiecte magice pe care eroii le pot folosii poate constitui un plus foarte mare jocului.
O aplicație care reușește să își țină utilizatorul mereu atent prin prezentarea de noi elemente
fiecărui joc este o aplicație care ar putea fi considerată de succes din punctul de vedere al
jucătorului.
55
7. Referințe
1. Pagina oficială BadLogicGames. [Online] https://libgdx.badlogicgames.com/.
2. Pagina Wikipedia pentru Mediul de dezvoltare Android. [Online]
https://en.wikipedia.org/wiki/Android_software_development.
3. Pagina Wikipedia pentru limbajul Java. [Online]
https://en.wikipedia.org/wiki/Java_(programming_language).
4. Pagina oficială Node.js. [Online] https://nodejs.org/en/about/.
5. Pagina Wikipedia pentru limbajul JavaScript. [Online] https://en.wikipedia.org/wiki/JavaScript.
6. Pagina Wikipedia pentru Socket.IO. [Online] https://en.wikipedia.org/wiki/Socket.IO.
7. Sebastian Di Giuseppe, Andreas Kruhlmann, Elmar van Rijnswou. Building a 3D Game with
LibGDX. 2016.
8. Pagina Wikipedia pentru algoritmul lui Dijkstra. [Online]
https://en.wikipedia.org/wiki/Dijkstra's_algorithm.