Framework pentru dezvoltarea de aplicații de tip CCG [306686]
Framework pentru dezvoltarea de aplicații de tip CCG
Proiect de diplomă
Flavius Robert ALB
Conducător științific:
sl. Dr. Ing. Ovidiu Flavius BANIAȘ
Cuprins
1 Introducere 5
Context …………………………………………………………………………………. 5
Motivație ……………………………………………………………………………….. 8
Descriere ………………………………………………………………………………. 8
1.3.1 Funcționalități generale …………………………………………………… 8
2 Stadiul actual și partea teoretică 9
2.1 Prezentare generală ……………………………………………………………….. 9
2.2 Tehnologii folosite …………………………………………………………………… 9
2.2.1 Unity ……………………………………………………………………………… 9
2.2.2 C# ……………………………………………………………………………….. 10
2.2.3 Doxygen ………………………………………………………………………..11
2.2.4 Trello …………………………………………………………………………….13
2.2.5 Jenkins ………………………………………………………………………… 14
2.2.6 Oracle VM VirtualBox ……………………………………………………. 16
2.2.7 Roslyn …………………………………………………………………………. 17
3 Arhitectura aplicației 19
3.1 Prezentare generală ………………………………………………………………. 19
3.2 Server ………………………………………………………………………………….. 20
3.2.1 Entities …………………………………………………………………….. 21
3.2.2 Game Types …………………………………………………………….. 22
3.2.3 Effects ……………………………………………………………………… 23
3.2.4 Turns ……………………………………………………………………….. 25
3.2.5 Holders……………………………………………………………………… 26
3.2.6 Logger ……………………………………………………………………… 27
3.3 Command …………………………………………………………………………….. 28
3.4 Client ……………………………………………………………………………………. 29
4 Implementarea 31
4.1 Mecanica jocului
4.1.1 Eroi/jucători ………………………………………………………………. 32
4.1.2 Împărțirea cărților ………………………………………………………. 33
4.1.3 Mecanica de Dragging ……………………………………………….. 34
4.1.4 Tabla de joc ……………………………………………………………… 35
4.1.5 Hover preview …………………………………………………………… 36
4.1.6 Rotația unei cărți ……………………………………………………….. 37
4.1.7 Scriptable Objects ……………………………………………………… 38
4.2 Infrastructura
4.2.1 Jenkins …………………………………………………………………….. 39
5 Testarea 40
5.1 Edit Mode ……………………………………………………………………………… 41
5.2 Play Mode …………………………………………………………………………….. 42
5.3 Jenkins …………………………………………………………………………………. 43
6 Concluzii 44
6.1 Direcții de dezvoltare …. ………………………………………………………….. 45
7 Referințe bibliografice 46
1 Introducere
Context
Lucrarea de diplomă descrie un Collectible Card Game(CCG) digital. CCG face parte din una din multiplele ramuri ale industriei dezvoltatoare de jocuri video. Această industrie este una omniprezentă atât în viețile copiilor cât și al adolescenților. Din totalul lor, un procent de 97% se joacă, doar în Statele Unite, pentru cel puțin o oră pe zi [1].
Această industrie a generat o sumă de $135 de miliarde în 2018, având o creștere de 10.9% față de anul precedent. Jocurile video pe mobil reprezintă 47% din această valoare, aducând astfel o sumă de $63.2 de miliarde, cu 12.8% mai mult decât anul precedent, această sumă fiind divizată între smartphones, aducând $50 de miliarde și tablete, $11.4 de miliarde. Calculatoarele reprezintă 25% din totalul acestei piețe, aducând o sumă de $33.4 de miliarde, cunoscând astfel o creștere de 3.2% față de anul precedent [2] (vezi Figura 1.1).
Figura 1.1. Anul 2018 în numere [3]
Din această sumă de $135 de miliarde, jocurile de tip Card Games au adus un venit de $705 de milioane în 2019, așteptându-se la o creștere de la an la an de 2.8% (vezi Figura 1.2). La nivel global, principala sursă de venit o reprezintă China (vezi Figura 1.3), cu $1.020 de milioane, iar relativ la populație, venitul per persoană în America este de 0.9$ [4] (vezi Figura 1.4).
Figura 1.2. Evoluția veniturilor jocurilor de tip CG
Figura 1.3. Principalele surse de venit la nivel global
Figura 1.4. Venitul per persoană in America
În acest moment pe piață, cel mai cunoscut joc digital de tip CCG este reprezentat de “Hearthstone”, al celor de la “Blizzard Entertrainment”, bucurându-se de un număr de 100 de milioane de jucători în noiembrie 2018 [5] (vezi Figura 1.5), cu un venit zilnic estimat la $12.000 și un număr de instalări zilnice estimat la 14.000 [6].
Figura 1.5. Evoluția numărului de jucători Hearthstone
Un alt joc reprezentativ a acestei subcategorii a jocurilor video o reprezintă
“Yu-Gi-Oh! Duel Links”, care în ciuda faptului că nu se bucură de același succes precum jocul discutat anterior, cunoaște un număr de aproximativ 10.000 zilnic [7].
1.2 Motivație
Pornind de la pasiunea pentru jocurile de strategie, mi-am propus conceperea unuia în care să se îmbine într-un mod uniform gândirea analitică, analiza în perspectivă cât și un nivel ridicat de competiție și divertisment.
În momentul de față jocurile asemănătoare existente pe piață, au ca lipsuri existența multiplelor suprafețe hexagonale care reprezintă tabla de joc, posibilitatea adăugării pe viitor a unui număr mai ridicat de jucători în decursul aceluiași meci și posibilitatea de a juca în diverse moduri.
Prin această lucrare se dorește umplerea cât mai mult a acestor goluri, printr-o arhitectură care facilitează adăugarea de moduri și jucători cu un minim de efort, punând astfel jocul în fața competiției la capitolul inovație.
1.3 Descriere
Lucrarea de diplomă constă într-un joc video de strategie tridimensional în care fiecare jucător începe cu un pachet de cărți, care pot fi atât creaturi cât și vrăji. Fiecare jucător începe cu o anumită cantitate de energie, care se incrementează la începutul fiecărei runde.
Fiecare carte din pachet are un cost de energie, un posibil efect, cât și o valoare de atac, respectiv defensivă în cazul creaturilor.
Tabla de joc reprezintă un număr de suprafețe hexagonale pe care jucătorii își pot convoca creaturile cu scopul de a cuceri în mod strategic zonele de influență ale jucătorului inamic și într-un final, de a-l distruge.
1.3.1 Funcționalități generale
Funcționalitățile aplicației sunt, după cum urmează:
Posibilitatea de a porni jocul împotriva calculatorului sau a altui jucător.
Posibilitatea de a juca o carte de tip creatură sau de tip vrajă.
Posibilitatea de a cuceri zone noi de influență.
Posibilitatea de a ataca atât creaturile cât și jucătorul inamic.
Posibilitatea de a-ți termina runda.
Posiblitatea de a vedea în detaliu cărțile jucate pe tablă.
Stadiul actual și parte teoretică
2.1 Prezentare generală
Acest proiect de diplomă este rezultatul unui cumul de informații din multiple domenii precum Inginerie Software, Game Development, DevOps cât și Project Planning.
Aceste domenii sunt reprezentate în această lucrare de tehnologiile folosite precum Unity, game engine-ul utilizat pentru a lega codul scris în spate în C# cu partea vizuală care face posibilă toată interacțiunea propriu-zisă a mecanicii jocului.
Trecând de partea concretă a implementării și ajungând în mediul infrastructurii, lucrarea are ca bază o combinație care are ca scop primordial creșterea eficacității timpului dedicat proiectării și a automatizării pe cât mai mult posibil a proceselor ce se află în concordanță cu nevoile proiectului. Această combinație este ca atare formată din tehnologii precum Trello, unde au fost planificați pașii ce au dus la maturizarea lucrării, alături de Git și Bitbucket, ce au făcut posibilă versionarea proiectului într-un mod cât mai concis și sigur.
Continuând în mediul infrastricturii, putem vorbi în continuare despre folosirea tehnologiei de tip CI/CB, Jenkins, care alături de editorul făcut posibil de Unity, combinat cu SMTP server și Bitbucket ne oferă log-uri complete referitoare atât la build-ul proiectului, cât și la testele aferente, pe adresa de e-mail la fiecare commit.
Pentru a ne permite un flux de lucru constant, inclusiv atunci când are loc un commit, serverul de Jenkins a fost configurat pe o mașină virtuală ce găzduiește o distribuție de Windows 10. Acest lucru a fost necesar deoarece editorul de Unity nu permite pornirea unei instanțe noi, în care se rulează testele, în paralel cu instanța existentă de lucru.
2.2 Tehnologii folosite
2.2.1 Unity
Unity este cel mai popular Game Engine din lume, jocurile făcute în acest IDE au ajuns pe aproximativ 3 miliarde de dispozitive global și au fost instalate de 24 de miliarde de ori în ultimele 12 luni [8]. Potrivit unuia din fondatorii engine-ului, David Helgason, Unity „este un toolset folosit pentru a construi jocuri și este tehnologia care execută grafica, sonorizarea, fizica, interacțiunile și rețelistica.” [9]
Deoarece jocurile video au la baza mecanicii și randării video termenul de cadru, Unity pune la baza programatorilor un set de metode standard printre care cele care se apelează atât la începutul jocului, de exemplu metodele Awake și Start, cât și per fiecare cadru în parte, cum ar fi metoda Update.
Unity vs Unreal Engine
Motivul pentru care s-a ales Unity pentru acest proiect de diplomă a fost bazat pe mai multe considerente.
În primul rând, limbajul de programare folosit în Unity este C#, în comparație cu C++, cel oferit de Unreal Engine, comparație ce va urma în secțiunea următoare.
Un alt lucru ce Unity oferă mai mult față de celălalt engine este comunitatea mult mai dezvoltată, oferind soluții pentru o gamă foarte largă și variată de probleme.
Un al treilea factor a fost dat de diferența nivelului de cunoștiințe pentru acest engine comparat cu oricare altul.
2.2.2 C#
Limbajul C# a fost inventat ca răspuns la necesitatea unui limbaj de programare modern care a permis aplicațiilor să țină pasul cu nevoile utilizatorilor. Limbajul a preluat cele mai bune rezultate din trecut, împrumutând totodata facilități atât din Java, cât și din C++, venind în același timp cu inovații proprii, cum ar fi delegările și indexările. Utilizând arhitectura .NET și fiind compilat într-un limbaj intermediar, codul rezultat este extrem de portabil [10].
De ce C#?
La ora actuală, cel mai folosit limbaj de programare utilizat în industria jocurilor video pe plan global este C++. Acest lucru este datorat nivelului de abstractizare mai scăzut, lucru posibil atât datorită limbajului, cât și a multitudinii de tool-uri existente.
În ciuda acestui considerent, s-a ales C# din două mari motive. În primul rând este limbajul principal suportat de Unity, limbajele auxiliare devenind între timp învechite, iar în al doilea rând nivelul familiarizării personale cu acest limbaj îl depășește pe acela de C++. Pentru o comparație mai detaliată vezi Tabelul 2.1.
Tabelul 2.1 Comparație între C++ și C#
2.2.3 Doxygen
Doxygen este un tool de generare a documentației pentru mai multe limbaje de programare printre care și C#.
Pornind de la comentariile lăsate de programator (vezi Codul 2-1), acesta generează fișiere HTML (vezi Figura 2.1, Figura 2.2) care conțin informații referitoare la clasa propriu-zisă, metodele, atributele cât și ierarhia clasei.
Codul 2-1. Exemplu de cod comentat
/// <summary>
/// Playing a spell card
/// </summary>
/// <param name="spellCardID">ID of the spell card</param>
/// <param name="targetID">ID of the target</param>
public void PlaySpell(int spellCardID, int targetID)
Figura 2.1. Ierarhia clasei Player
Figura 2.2. Generat din comentariile metodei de la Codul 2-1
2.2.4 Trello
Trello este un tool folosit pentru Project Planning care oferă funcționalități de bază precum Kanban Board, planificarea sarcinilor de lucru tinând cont de un termen limită, putând totodată adăuga membrii, subcerințe, comentarii, precum și a face legătura cu un commit propriu-zis.
Trello vs Jira
Jira este cel mai folosit tool de Project Planning, dezvoltat de Atlassian care îmbină o gamă largă de configurații ce pot fi modificate și personalizate fiecărei echipe de dezvoltare în parte.
Tabelul 2.2. Comparație între Trello și Jira [11]
Figura 2.3. Kanban Board-ul folosit.
Figura 2.4 Carte în care se poate creea o sarcină în Trello.
2.2.5 Jenkins
Jenkins este un server de automatizare open source, scris în Java. Acesta ajută în accelerarea procesului de dezvoltare software, prin automatizarea diferitelor procese, cum ar fi etapele de build, testare, analiză statică a codului și deploy [12].
Pentru o automatizare urmând etapele de build, testare și deploy, etapele ar fi următoarele:
În cadrul etapei de build, codul este verificat pentru posibile erori sau warning-uri. În cazul unei erori, Jenkins notifică utilizatorul sau echipa pentru a lua măsuri cu scopul reparării și revenirii în stadiul optim al software-ului.
Dacă etapa de build s-a terminat cu succes, urmează etapa de testare, care constă în verificarea pas cu pas a tuturor testelor în cadrul aplicației.
Din nou, în urma unui succes, se trece în etapa următoare, cea de deploy, care, în funcție de context, se poate referi la procesul de publishing a aplicației în mediul de producție.
Jenkins vs CircleCI
Un alt sistem de integrare continuă folosit este CircleCI, un sistem de tip cloud-based, adică nu are nevoie de server dedicat pentru rularea acestuia. Acesta permite ușurința executării proceselor de configurare și întreținere.
CircleCI suportă toate tipurile de testare software, inclusiv mediilor de dezvoltare de tip web, mobil, desktop și container. Acesta permite integrarea tehnologiilor GitHub, Amazon EC2, dotCloud, Slack, Docker, etc.
CircleCI este folosit de un număr ridicat de companii, precum Facebook, Spotify, Kickstarter [13].
Tabel 2.3 Comparație între Jenkins și CircleCI
Figura 2.5 Build curent al proiectului de diplomă.
Figura 2.6 Log-ul build-ului.
Figura 2.7 Interfața build-ului folosind plugin-ul Blue Ocean
2.2.6 Oracle VM VirtualBox
Oracle VM VirtualBox este un produs folosit pentru crearea, administrarea și rularea mașinilor virtuale. Acestea sunt calculatoare ale căror componente hardware sunt emulate de către calculatorul gazdă. VirtualBox permite crearea mai multor mașini virtuale în paralel și rularea acestora asincron cu rularea calculatorului gazdă.
Oracle VM VirtualBox vs VMWare
Un alt tool de virtualizare folosit la nivel global este VMWare. Acesta este lider la nivel mondial în tehnologii de virtualizare, datorită faptului că pot să întrețină tehnologii de virtualizare la scară largă.
VMWare deține o gamă largă de distribuții de mașini virtuale, tool-uri de dezvoltare third-party și programe de distribuție bine dezvoltate [14].
Tabelul 2.4. Comparație între Oracle VM VirtualBox și VMWare
Windows 10 vs CentOS
Pentru proiectul de diplomă am folosit o mașină virtuală în care am instalat Windows10, deoarece are suport mai bun pentru automatizarea build-ului în Jenkins, în CentOS nefiind posibilă instalarea unui editor Unity.
2.2.7 Roslyn
Roslyn este un set de compilatore open-source și un analizator de cod static creat de către Microsoft, folosit pentru aplicații scrise în C# și Visual Basic, pentru arhitecturile Mono și .NET.
Roslyn este integrat în IDE-ul Visual Studio, iar comenzile acestuia pot fi executate atât din interiorul IDE-ului, cât și prin intermediul liniei de comandă.
Figura 2.8 Calculul indicatorilor de performanță a codului
Figura 2.9 Rularea analizei statică a codului.
Roslyn are un set de metrici standard, însă fiecare programator își poate creea propriile metrici de analiză a codului, în interiorului fișierelor XML, acestea fiind mai apoi importate în interiorul IDE-ului.
Setul de metrici standard oferit de Roslyn include:
– cyclomatic complexity: contorizează numărul de căi independente a unei funcții; arată numărul minim de teste necesare a fi scrise, însă intepretarea nu poate aduce direct îmbunătățire; pragurile de complexitate se pot încadra ca fiind între 1 și 10 reprezentând un risc scăzut, de la 11 la 20 risc moderat, între 21 și 50 un cod riscant cu o logică complexă și peste 50 cod foarte complex cu risc la fel de ridicat.
– lines of code: contorizează numărul de linii de cod existente într-o clasă.
– depth of inheritance: reprezintă adâncimea maximă a unei clase într-o ierarhie de clase; moștenirea este măsurată însă doar impactul potențial, nu cel real este cuantificat.
cu cât nivelul este mai mare cu atât înțelegerea clasei este mai grea de perceput.
cu cât nivelul este mai mare, cu atât clasa este mai complexă.
cu cât nivelul este mai mare, cu atât crește și potențialul de refolosire a clasei părinte.
– class coupling: arată numărul de clase de unde sunt folosite metode sau atribute; numărul de alte clase cuplate cu clasa măsurată; ia în considerare dependințele reale, nu doar cele declarate, însă nu face nicio diferență între cuplarea tipurilor și/sau intensităților.
cu cât cuplajul este mai puternic, cu atât refolosirea clasei într-o altă aplicație este mai mică, modularitatea clasei scade.
cu cât cuplajul este mai puternic, cu atât clasa este mai sensibilă la schimbări.
cu cât cuplajul este mai puternic, cu atât mai multe teste mai riguroase trebuiesc scrise.
– maintainability index: calculează un index între 0 și 100 care reprezintă ușurința cu care codul poate fi menținut; o valoare de index mare reprezintă o menținere mai ușoară, având ca specific culoarea verde, indexul fiind încadrat între valorile 20 și 100. O culoare galbenă este specifică unei game de valori între 10 și 19, reprezentând o menținere mediocră, iar o culoare roșie înseamnă o menținere greoaie.
– în cadrul proiectului de diplomă metricile de mai sus sunt mapate astfel:
maintainability index variază între valoarea minimă de 52 și valoare maximă 100
cyclomatic complexity conține valori între 0 și 65.
depth of inheritance conține valori între 1 și 6.
class coupling conține valori între 0 și 51.
3 Arhitectura aplicației
3.1 Prezentare generală
Arhitectura aplicației a fost gândită în jurul cuvântului schimbare. Astfel, a fost necesar un design care să permită adăugarea respectiv modificarea componentelor/modulelor într-un mod cât mai facil, necesitând cât mai puține modificări exterioare.
Proiectul a fost gândit în vederea posibilității de a juca în rețea cu alți jucători, lucru ce în urma unor analize amănunțite s-a ajuns la concluzia că momentan nu este suficient de fezabil, dat fiind faptul că API-ul oferit de Unity în momentul de față este unul învechit, care urmează să fie schimbat, motiv pentru care s-a decis ca timpul posibil investit într-o necesară schimbare ulterioară să fie redistribuit adăugării de noi funcționalități și îmbunătățirii arhitecturii existente.
Plecând astfel de la această idee, jocul a fost structurat în 2 mari segmente:
– codul server care conține logica propriu-zisă.
– codul client care efectuează animațiile respectiv tot ce ține de partea vizuală în funcție de rezultatele și de parametrii primiți de la server.
Legătura dintre client și server este facilitată strict prin intermediul comenzilor, având următoarea structură:
Figura 3.1 Legătura Server – Client
3.2 Server
Partea de server se ocupă de toată logica și toată mecanica de bază existentă în aplicație. Ca atare, aici a fost necesară investiția celei mai mari porțiuni de timp atât în implementarea propriu-zisă, cât și în construcția arhitecturii vizând totodată viitoarele extensii posibile.
Server-ul este alcătuit din mai multe module, conform Figurii 3.2.
Figura 3.2. Principalele module ale server-ului.
Toate aceste module sunt îmbinate într-un mod care facilitează cât mai ușor adăugarea de noi extensii, respectiv funcționalități, fiecare fiind descris succint în subcapitolele ce urmează.
3.2.1 Entities
Entitățile reprezintă modelele propriu-zise din cadrul jocului. Acestea reprezintă de fapt, obiectele care interacționează cel mai mult în cadrul jocului existând o legătură directă între ele.
Concret, ele sunt:
– Player-ul
– Creatura
– Cartea de joc
Entitatea de Player deține informațiile referitoare la cărțile deținute atât în deck, în mână, pe tabla de joc, cât și referitoare la nivelul de energie al jucătorului și viața sa.
Ca acțiuni, Player-ul este capabil de a-și termina tura, a trage o carte, a juca o carte de vrajă, una de creatură și a-și folosi puterea specifică personajului.
Referitor la entitatea ce reprezintă logica unei creaturi, aceasta deține ca informații jucătorul ce o deține, viața sa, daunele sale, efectul specific cât și numărul de atacuri și mișcări intr-o tură.
Ca acțiuni, o creatură poate să atace jucătorul, o altă creatură sau să se deplaseze pe o altă suprafață de joc.
O carte de joc conține ca informații jucătorul ce o deține, efectul specific cărții și costul de energie, iar ca proprietate această carte ne spune dacă poate fi jucată.
Pentru a face posibilă adăugarea pe viitor a mai multor entități, dacă e necesar, s-a decis crearea unor interfețe care să conțină informații specifice atât tuturor entităților și anume ID-ul, devenit obligatoriu prin contractul interfeței IIdentifiable, cât și a entităților care au efectiv un rol pe tabla de joc, rol comun tuturor astfel de obiecte conținând viața sa, cât și acțiunea de a muri devenite și acestea obligatoriu prin contractul intefeței IEntity.
Această structură este astfel concluzionată în Figura 3.3, atrătând la un nivel abstract atât clasele cât și interfețele care intră în alcătuirea ierarhiei.
Figura 3.3. Relația dintre entități
3.2.2 Game Types
Pentru a putea adăuga respectiv schimba modul de joc în cel mai ușor fel cu putiință, fară a fii nevoiți să modificăm într-un număr relativ mare de clase, a fost necesară o arhitectură care să permită aceste modificări având ca și concluzie doar necesitatea creării unei clase specifice modului nou care să găzduiască implementarea necesară cât și modificarea unei interfețe și a unei alte clase.
Astfel această ierarhie este formată dintr-un Singleton, ce cu ajutorul unui Factory Method intern se comportă pe post de Proxy pentru diversele moduri de joc posibile, pe lângă care mai există și o ierarhie de două interfețe și o clasă specifică modului de joc pentru doi jucători.
Configurarea unui mod de joc are loc în metoda de Awake specifică clasei de TurnManager care are ca scop inițializarea jocului la start. (vezi Codul 3.1)
Codul 3.1. Inițializarea modului de joc
PlayerManager.CreateTwoPlayerGame(FindObjectsOfType<Player>());
Figura 3.4. Arhitectura Game Type.
3.2.3 Effects
În momentul în care o carte de tip vrajă sau anumite cărți de tip creatură sunt jucate, anumite efecte se pot activa, efecte care au ca rezultat schimbarea punctelor de viață a unei creaturi sau a jucătorului, tragerea unei cărți din teanc ș.a.
Aceste efecte sunt astfel împărțite în două categorii, cele specifice creaturilor și cele specifice vrăjilor.
Luând ca primă ierarhie efectele specifice creaturilor obținem astfel o arhitectură ce are ca și componente un Factory Method, un număr de interfețe, o clasă abstractă și efectele propriu-zise.
Ierarhia astfel obținută ne permite să activăm toate efectele posibil existente pe entități la momentul jucării lor, de exemplu, separându-le totodată de celelalte posibile efecte care se activează la evenimente diferite.
Această structură are la bază existența tuturor situațiilor în care un efect poate fi activat în modul standard și anume la crearea entității, la moartea ei, cât și în anumite momente din joc. Aceste moduri standard sunt implementate ca fiind goale în clasa abstractă, iar în cazul în care un efect concret are nevoie de o astfel de metodă la activare, suprascrierea metodei specific este suficientă.
Pentru efecte care au loc în cazuri excepționale, fiind necesară înregistrarea evenimentului la creare, efectul fiind activat doar în anumite situații, simpla implementare a interfeței ICEEvent și implicit implementarea metodelor specifice contractului este suficientă. Întreaga arhitectură este prezentată în Figura 3.5.
Continuând cu cea de-a doua ierarhie, cea reprezentând efectele specific cărților de tip vrajă, respective puterilor jucătorilor, arhitectura constă într-un Factory Method, o interfață, o clasă abstractă și efectele propriu-zise.
Ierarhia astfel obținută este una simplă care permite adăugarea oricând a unor efecte fără a fi necesară modificări exterioare ierarhiei curente. Întreaga arhitectură este prezentată în Figura 3.6.
Figura 3.5 Ierarhia de clase reprezentând efectele specifice creaturilor
Figura 3.6 Ierarhia de clase reprezentând efectele specifice vrăjilor
3.2.4 Turns
Pentru a putea face trecerea de la jucător la AI într-un mod foarte facil s-a mers pe un design de tip Strategy, lucru datorat moștenirii și conceperea unei clase părinte de tipul abstract care să conțină codul comun. Astfel, prin simplul apel al metodei virtuale, metodă suprascrisă în fiecare din subclase, configurarea entităților la începutul turei se poate face la Runtime, nefiind necesar a se ține cont de entitatea concretă.
După cum se poate observa și în schema din Figura 3.7, jocul oferă posibilitatea jucătorului de a se juca împotriva unei inteligențe artificiale. Deși un AI la nivel de bază, la începutul fiecărei runde ale sale, acesta își alege o strategie din punct de vedere aleatoriu, iar pe urmă este capabilă să joace o carte, să atace o creatură, respectiv să își folosească puterea specifică eroului, toate aceste lucruri făcându-l suficient pentru un adversar de bază.
Figura 3.7 Ierarhia de clase reprezentând modulul Turns.
3.2.5 Holdere
Holderele, după cum spune și numele sunt niște clase simple cu o funcționalitate de bază, pe același principiu cu POJO-urile, a căror scop este simpla menținere a datelor specifice fiecărei clase. De exemplu clasa de Hand conține o listă cu toate obiectele de tip CardLogic care se găsesc în mâna jucătorului.
Deși clasele de Hexagons, Hand respectiv Deck dețin informații specifice fiecărui jucător în parte, clasa de tip Singleton, EntityManager deține informațiile tuturor obiectelor de tip IIdentifiable din întregul joc.
Figura 3.8 Conținutul modulului de tip Holders.
3.2.6 Logger
Pentru a depana jocul într-un mod cât mai ușor și rapid, vizând totodată cât mai multe informații, respectiv gravitatea anumitor probleme ce pot apărea, a fost gândită și implementată o structură cu scop de Logger. Unity oferă în API-ul lor o astfel de structură însă am considerat că modul ei de folosință este undeva prea complicat și prea greu de modificat, respectiv modela după propriile necesități.
Astfel a luat naștere arhitectura din Figura 3.9, care permite adăugarea după necesitate și după bunul plac a unor niveluri custom de logare, a unor filtrări mai specifice, a unor diverse moduri de formatare a log-urilor și a posibilității de scriere a acestor informații în multiple moduri.
Toate aceste lucruri pot fi modificate la Runtime, iar introducerea unui Logger într-un fișier se bazează pe un Factory Method din clasa abstractă SLogger, care returnează un Logger ce conține numele specific clasei. (vezi Codul 3.2)
Pentru a folosi un Logger astfel introdus, codul poate arăta sub forma codului 3.3.
Codul 3.2 Inițializarea unui Logger în clasa Player.
private static readonly SLogger LOGGER = SLogger.GetLogger(nameof(Player));
Codul 3.3 Modul de folosire a unui Logger.
LOGGER.Log(Level.FINE, "Player's " + this.ToString() + " amount of energy he has this turned has changed",
new Param[]
{
new Param { Name = nameof(energyThisTurn), Value = energyThisTurn },
new Param { Name = nameof(EnergyThisTurn), Value = value }
});
Figura 3.9. Arhitectura Logger.
3.3 Command
Legătura dintre server și client este susținută de o arhitectură ce are la bază pattern-ul Command, șablon de proiectare ce desparte logica de partea vizuală, permințând astfel un nivel ridicat de modularitate în cadrul proiectului.
Astfel, pentru a putea atât instanția cât și declanșa o acțiune pe client, a cărei logică s-a întamplat deja pe server, simpla creare a unui obiect de tipul acelei comenzi specifice și adăugarea ei în coada de comenzi, această coadă fiind declanșată ulterior, este suficientă.
Pentru a crea o comandă specifică nevoii, simpla extindere a clasei Command și implicit suprascrierea metodei StartCommandExecution este suficientă.
În interiorul clasei Command se găsește o coadă statică care conține toate comenzile și metodele specifice adăugării unei comenzi în coadă, respectiv declanșarea uneia în format FIFO. (vezi Figura 3.10)
Figura 3.10 Arhitectura modulului Command.
3.4 Client
Clientul constă în multiple clase fără a fi necesar dependente unele de altele. Scopul acestor clase este acela de a reprezenta din punct de vedere vizual prin animații sau imagini într-un mod cât mai reprezentativ logica din spate. Astfel clientul nu cunoaște ce se întâmplă pe server, el primește de la acesta doar ID-uri sau alte tipuri primitive.
În Figura 3.11 se pot observa trei dintre aceste clase, fiecare având propriul scop și ocupându-se de o anumită funcționalitate specifică.
Figura 3.11 Exemplu de clase existente pe Client.
Tot pe Client, potrivit Figurii 3.12 se poate observa clasa specifică fiecărui jucător care deține informațiile referitoare la partea vizuală a pachetului jucătorului, energia sa, partea vizuală a cărților din mână, portretul, abilitatea specifică fiecărui erou, cât și partea vizuală a hexagoanelor. Toate cele de mai sus se află de fapt în Unity, accesând direct Editor-ul pe care acesta ni-l pune la dispoziție, incluzând poziția obiectelor în scenă, scalarea lor și alte informații referitoare la interfața grafică.
Figura 3.12 PlayerArea specific fiecărui jucător
De asemena, în fiecare clasă se poate observa folosirea metodelor Awake, Start și Update, de care s-a vorbit în capitolele anterioare, iar în plus, în Figura 3.13 se pot găsi metodele OnMouseUp și OnMouseDown care se apelează în momentul în care utilizatorul dă click respectiv lasă de mouse.
Ultimele două metode standard oferite de Unity sunt folosite în ierarhia de tip Dragging, care constă într-o clasă, Draggable, care apelează metodele specifice acțiunii de click pentru fiecare tip de drag specific în parte.
Figura 3.13. Ierarhia de clase de tip Dragging.
4 Implementare
După cum a fost precizat în capitolele anterioare și detaliat la un nivel abstract în cadrul prezentării arhitecturii, framework-ul prezent a fost gândit astfel încât să fie deschis la adăugare de module noi sau modificare a celor existente.
Astfel, de la început a fost necesară o delimitare concretă între entități, diversele moduri de Dragging, afișajul vizual și efectele existente în joc, cât și față de modurile de joc posibile.
4.1 Mecanica jocului
În următoarele subcapitole vor fi prezentate în mod succint și detaliat câteva dintre cele mai semnificative mecanici și efecte ale jocului împreună cu modul de gândire, de implementare și deciziile luate pentru a le face posibile.
4.1.1 Eroi / jucători
Primul lucru care are loc în momentul în care un joc începe este deciderea ordinii jucătorilor, decidere ce se întâmplă într-un mod aleator.
Odată decisă această ordine, din punct de vedere vizual fiecare dintre cele două caractere specifice eroilor jucătorilor revine în poziția specifică pe tabla de joc, iar următorul pas de împărțire a cărților este declanșat.
Figura 4.1 Poziția eroilor la începutul jocului.
Figura 4.2 Poziția eroilor după finalizarea deciderii ordinii.
4.1.2 Împărțirea cărților
După ce a avut loc deciderea ordinii jucătorilor, urmează împărțirea cărților. Fiecare pachet de cărți ale fiecărui jucător conține atât cărți unice în joc, cât și cărți comune. Înainte de împărțirea propriu-zisă, fiecare pachet de cărți este amestecat, iar mai apoi fiecare jucător primește câte o carte de deasupra pachetului, până la un maxim de 4 cărți, iar cel de-al doilea jucător primește o carte ce oferă un cristal de energie bonus, carte ce costă 0 unități de energie.
Figura 4.3 Extragerea cărților din pachet la începutul meciului.
Figura 4.4 Extragerea unei cărți la începutul fiecărei runde.
4.1.3 Mecanica de Dragging
Deoarece există multiple metode de Drag and Drop în joc, există o arhitectură care să permită scalarea și folosirea într-un mod cât mai facil a acestor metode.
Există multiple metode astfel implementate pentru mecanica de drag, printre care și DragCreatureOnTable care se ocupă cu drag-ul unei creaturi pe tabla de joc, DragCreatureAttackOrMove care se ocupa cu drag-ul unei creaturi în momentul de atac sau de mișcare pe o anumită suprafață pe tabla de joc sau DragSpellOnTarget care se ocupă cu drag-ul unei cărți de tipul vrăji asupra unei țințe cu scopul de a crea daune.
Luând ca exemplu clasa de DragSpellOnTarget și explicată într-un mod relativ succint, punctele cheie astfel obținute sunt:
– în momentul în care jucătorul face click pe o carte de tipul vrajă care conține clasa respectivă ca și componentă, primul lucru care se verifică este posibilitatea jucării cărții selectate;
– în cazul afirmativ, începutul de Drag are loc, cartea aflându-se astfel într-o stare vizuală specifică Drag-ului, urmând să fie vizibilă săgeata și ținta;
– la fiecare cadru, presupunând că jucătorul încă se află în stadiul de Dragging, se calculează poziția țintei relativ la poziția mouse-ului, iar dacă distanța minimă față de carte a fost îndeplinită, această țintă este și vizibilă. Fiind un calcul ce are loc la fiecare cadru, această distanță și poziție este calculată constant, existând o corelație între locația ei și locația mouse-ului;
– la finalul Drag-ului, când jucătorul lasă de click, ținta și săgeata dispar, iar mai apoi se verifică obiectul asupra căruia s-a lăsat de click, iar în cazul în care acest obiect este de tipul jucător sau creatură, iar opțiunile de atac ale cărții vizează acest obiect, are loc crearea de daune obiectului respectiv.
Figura 4.5 Mecanica de Drag.
Celelalte moduri de tip Dragging funcționează într-un fel asemănător însă pentru cazuri specifice fiecăreia în parte.
4.1.4 Tabla de joc
Tabla de joc constă în suprafețe de formă hexagonală îmbinând o mecanică de influență. Influența reprezintă proprietarul fiecărei suprafețe de joc în parte, în cazul unui joc între doi jucători, existând 3 astfel de proprietari: jucătorul 1, jucătorul 2, suprafețe neutre.
Fiecare jucător își poate amplasa cărțile de tipul creatură doar pe suprafețele deținute de el, iar în momentul în care o astfel de creatură a fost așezată, toate suprafețele neutre din împrejur vor ajunge sub influența acelui jucător.
Un alt mod de a-ți extinde influența este dat de mișcarea unei creaturi pe tabla de joc, putând converti toate celelalte suprafețe la influența celui care o deține prin simpla deplasare a sa.
Există de asemenea două tipuri de suprafețe pe tabla de joc, una fiind accesibilă tuturor creaturilor, iar cea de-a doua doar creaturilor ce au posibilitatea de a zbura.
Configurația tablei de joc este diferită la fiecare început de joc.
Figura 4.6 Creaturi pe tabla de joc.
Figura 4.7 Mișcarea unei creaturi cu scopul de a ocupa suprafața neutră
4.1.5 Hover preview
În momentul în care jucătorul se apropie cu mouse-ul de o carte din mână sau de orice creatură de pe tabla de joc, cartea specifică va apărea lângă la o scară mai mare cu scopul de a arăta jucătorului toate informațiile relevante.
Figura 4.8 Hover-ul unei creaturi.
4.1.6 Rotația unei cărți
Un alt efect existent și plăcut vizual care dă jucătorului un sentiment de apartenență îl reprezintă efectul vizual legat de rotația card-ului. Când o carte este extrasă din pachet, ea este scoasă cu spatele, urmând să fie rotită până ajunge în mâna jucătorului.
Mecanica funcționează în felul următor:
– grosimea cărții este de aproximativ 0.01 unități în Unity, iar în spatele cărții se găsește un punct la o distanță de 0.01;
– la fiecare cadru se trimit raze de la cameră spre carte pe o distanță maximă egală cu distanța dintre cameră și acel punct;
– în cazul în care acea rază lovește cartea înseamnă că cartea se află cu spatele la cameră;
– în cazul în care raza nu lovește cartea înseamna că acel punct se găsește între cameră și carte, iar acest lucru ne spune că acea carte este cu fața la cameră.
4.1.7 Scriptable Objects
Unity oferă posibilitatea creării unor obiecte generice având anumite atribute, obiecte care pot fi refolosite reprezentând informații specifice. Aceste obiecte se numesc Scriptable Objects, iar în cazul acestui proiect de diplomă, ele au fost folosite pentru cărți și personaj, fiecare astfel de obiect fiind creat pentru un tip precizat mai sus.
Acest obiect este ”citit” din scripturile specifice entităților, citire care inițializează atributele vizuale ale fiecărui model.
Aceste clase sunt echivalentul POJO-urilor din Java, conținând doar atribute, fără vreo logică necesară implementării.
Figura 4.9 Exemplu de Card Asset
/*!
* \brief Contains all the data card specific
*/
public class CardAsset : ScriptableObject
{
[Header("General info")]
public CharacterAsset CharacterAsset; //!< Class specific card; if this is null, it`s a neutral card
[TextArea(2,3)]
public string Description; //!< Card's description
public Sprite CardImage; //!< Card's splash image
public int EnergyCost; //!< Energy cost for the card to be played
public Rarity CardRarity; //!< Rarity type of the card
[Header("Creature Info")]
public int Defence; //!< Creature's defence value; if it's 0 it's a spell card
public int Attack; //!< The attack value of the creature
public int AttacksForOneTurn = 1; //!< Number of attacks per turn
public int MovementForOneTurn = 1; //!< Number of tiles you can move per turn
public bool Taunt; //!< Boolean for Taunt effect
public bool Charge; //!< Boolean for ChargeEffect
public bool Flying; //!< Boolean for Flying effect
public bool Range; //!< Boolean for Range effect
public bool Infrastructure; //!< Boolean for Infrastucture effect. Infrastructure cards can only be placed on certain tiles
public CreatureEffectType CreatureEffect; //!< The type of creature effect existing on the card
public int CreatureEffectValue; //!< The value the effect has, e.g.: deals [value] damage
[Header("Spell Info")]
public SpellEffectType SpellEffect; //!< A spell effect that will take place when the card is played
public int SpellEffectValue; //!< The value the effect has, e.g.: deals [value] damage / heal
public TargetingOptions Targets; //!< The targeting options the spell has
}
Codul 4.1 Clasa Card Asset
4.2 Infrastructură
Trecând de partea de logică conținând implementarea ajungem la partea de infrastructură, parte ce are ca scop principal automatizarea proceselor de testare și build-ul proiectului propriu-zis.
Pentru o identificare și o analiză ușoară a posibilelor erori, rezultatele proceselor de testare și build sunt salvate într-un fișier de log, iar aceste log-uri sunt trimise, prin intermediul protocolului SMTP, pe o adresă de mail configurată în interiorul pipeline-ului.
4.2.1 Jenkins
Pentru a putea folosi acest tool a fost necesară configurarea lui, configurare ce a constat în adăugarea de plugin-uri specifice atât editorului de Unity, lucru ce a făcut posibilă apelarea acestui editor din linia de comandă, cât și plugin-uri specifice serviciului de Git și Bitbucket.
După ce toate aceste plugin-uri au fost configurate, s-a creat conexiunea efectivă dintre Jenkins și Bitbucket, conexiune cunoscută sub numele de ”webhook”, care a fost setată pentru a verifica repository-ul de pe site-ul de versionare la fiecare două minute, iar în cazul în care a avut loc un commit, se rulează pipeline-ul implementat.
După ce un pipeline-ul descris mai sus se finalizează, rezultatele obținute, constând într-un log pentru build și unul pentru teste, sunt trimise pe email-ul specificat în plugin-ul de SMTP server din cadrul Jenkins.
Rularea testelor în Unity necesită o instanță de editor, iar rularea testelor nu se poate face în paralel cu dezvoltarea aplicației, motiv pentru care a fost necesară o abordare care să permită dezvoltarea într-un flux continuu în același timp cu rularea testelor declanșate de un posibil commit.
S-a ajuns astfel la folosirea unei mașini virtuale cu o distribuție de Windows 10 pe care s-a instalat atât Jenkins cât și Unity, folosită strict în momentele de commit.
Această implementare s-a concluzionat astfel cu un flux de lucru constant, având ca puncte cheie lucrul continuu la dezvoltare, rularea testelor și aflarea statusului aplicației la fiecare commit.
Figura 4.10. Build step-urile în Jenkins.
Figura 4.11. Mailul cu fișierele log trimis după încheierea rulării pipeline-ului
5 Testare
După cum a fost precizat și în capitolul anterior, am implementat Jenkins pentru că este un tool de automatizare a întregului proces de dezvoltare a unui software, proces ce pentru a avea o siguranță constantă și o integrare continuă consistă în crearea de teste.
Testele în Unity sunt de două feluri, Edit Mode, echivalentul testelor unitare, respectiv testele de tip Play Mode, echivalentul celor de integrare, ele fiind scrise folosind framework-ul NUnit.
Acest framework oferă metode specifice setării scenei, de exemplu, înaintea rulării tuturor testelor, la finalul rulării lor, cât și adnotările specifice reprezentării testelor. Ca și în orice framework de testare și aici întâlnim bine cunoscutele metode de tip Assert.
Fiind vorba despre obiecte ce conțin ierarhii, pentru a găsi o subcomponentă specifică unui obiect din scenă, folosind metoda de Find oferită de Unity și specificând locația, descriind toate nivelurile până la ea și despărțite prin ”/”, putem prelua cu ușurință acea componentă.
5.1 Edit Mode
Testele de acest gen sunt statice, asta însemnând că obiectele testate aici au starea specifică unui singur cadru. Aceste teste au scopul de a verifica atribute aflate pe obiectul propriu-zis și în ierarhia acestuia.
//Spells
[Test]
public void _Assasinate()
{
var card = GameObject.Find("Assasinate");
//Title
var titleText = GameObject.Find("Assasinate/Canvas/CardPanel/CardFace/CardTitle/CardTitleText");
Assert.AreEqual("Assasinate", titleText.GetComponent<Text>().text);
//Energy
var energyText = GameObject.Find("Assasinate/Canvas/CardPanel/CardFace/PS_Energy/EnergyText");
Assert.AreEqual("5", energyText.GetComponent<Text>().text);
//Description
var descriptionText = GameObject.Find("Assasinate/Canvas/CardPanel/CardFace/CardBody/Description/DescriptionText");
Assert.AreEqual("Destroy an enemy minion.", descriptionText.GetComponent<Text>().text);
//Rarity
var rarityText = card.GetComponent<OneCardManager>().CardAsset.CardRarity;
Assert.AreEqual("Common", rarityText.ToString());
}
Codul 5.1 Exemplu de test unitar
Figura 5.1. Tab-ul specific rulării testelor de tip EditMode
5.2 Play Mode
Testele de tipul PlayMode sunt teste dinamice, fiind teste a căror stare se schimbă pe parcursul a mai multor cadre verificându-se astfel integrarea obiectelor cu scena de joc.
Aceste teste sunt cele vitale funcționării corecte a aplicației deoarece se pot testa mecanici, funcționalități, fie luate independente, fie în combinații. Pentru a rula aceste teste într-un mod cât mai fidel cu realitatea, freamwork-ul pune la dispoziția programatorilor metodele de Wait sau de Timeout, metode ce se apelează pe decursul mai multor cadre.
[UnityTest]
public IEnumerator _Damage_Effect()
{
GameObject prefab = Resources.Load("Test/DamageEffect", typeof(GameObject)) as GameObject;
DamageEffect.CreateDamageEffect(new Vector3(0, 0, 0), 3, prefab);
yield return new WaitForSeconds(1f);
Assert.Null(GameObject.Find("DamageEffect"));
Assert.Pass();
GameObject.Destroy(prefab);
}
Codul 5.2. Exemplu de test de integrare.
Figura 5.2 Tab-ul specific rulării testelor de tip PlayMode
5.3 Jenkins
Pentru a face posibilă rularea testelor într-un mod cât mai ușor și cât mai integrat cu restul dezvoltării aplicației, s-a decis folosirea tool-ului de automatizare Jenkins.
Astfel, după cum a fost precizat într-un mod cât mai concis și detaliat la capitolul Implementare, scopul acestui tool este a de a rula toate testele existente în proiect la fiecare commit în vederea integrării noilor modificări cu restul aplicației. Rezultatele testelor astfel rulate este automat trimis pe e-mail, de unde în urma citirii log-urilor se poate detecta anumite situații precare, respectiv posibile eșecuri, facilitând astfel depanarea și repararea locurilor în care commit-ul ar fi putut cauza astfel de comportamente.
Un astfel de log ce conține rezultatele testelor se poate observa în Codul 5.3 unde pe lângă numele clasei de care aparține testul și starea testului mai există și informații referitoare la ora de începere, de final și numărul de Assert-uri incluse în metodă.
< test – suite type = "TestFixture" id = "1001" name = "CardsTest" fullname = "CardsTest" classname = "CardsTest" runstate = "Runnable" testcasecount = "28" result = "Passed" start – time = "2019-06-06 15:10:02Z" end – time = "2019-06-06 15:11:08Z" duration = "66.355473" total = "28" passed = "28" failed = "0" inconclusive = "0" skipped = "0" asserts = "158" >
< test -case id = "1002" name = "_Assasinate" fullname = "CardsTest._Assasinate" methodname = "_Assasinate" classname = "CardsTest" runstate = "Runnable" seed = "2144037618" result = "Passed" start – time = "2019-06-06 15:10:02Z" end – time = "2019-06-06 15:10:07Z" duration = "4.988884" asserts = "4" />
< test -case id = "1022" name = "_Bounty_Huntress" fullname = "CardsTest._Bounty_Huntress" methodname = "_Bounty_Huntress" classname = "CardsTest" runstate = "Runnable" seed = "1316390606" result = "Passed" start – time = "2019-06-06 15:10:07Z" end – time = "2019-06-06 15:10:09Z" duration = "2.205128" asserts = "6" />
< test -case id = "1006" name = "_Cataclysm" fullname = "CardsTest._Cataclysm" methodname = "_Cataclysm" classname = "CardsTest" runstate = "Runnable" seed = "705313216" result = "Passed" start – time = "2019-06-06 15:10:09Z" end – time = "2019-06-06 15:10:12Z" duration = "2.290405" asserts = "4" />
< test -case id = "1010" name = "_Eon_Shadow" fullname = "CardsTest._Eon_Shadow" methodname = "_Eon_Shadow" classname = "CardsTest" runstate = "Runnable" seed = "994953993" result = "Passed" start – time = "2019-06-06 15:10:18Z" end – time = "2019-06-06 15:10:20Z" duration = "2.175302" asserts = "6" />
< test -case id = "1011" name = "_Eon_Tech_Engineer" fullname = "CardsTest._Eon_Tech_Engineer" methodname = "_Eon_Tech_Engineer" classname = "CardsTest" runstate = "Runnable" seed = "182407140" result = "Passed" start – time = "2019-06-06 15:10:20Z" end – time = "2019-06-06 15:10:22Z" duration = "2.168894" asserts = "6" />
Codul 5.3. Conținut al fișierului cu rezultatele testelor
6 Concluzii
După cum a fost relatată întreaga arhitectură și prin modul în care dezvoltarea este susținută de infrastructura pe care o are la bază, acest framework conține toate punctele cheie, atât necesare dezvoltării unui joc video de tip CCG, cât și extinderii platformei într-un mod ce necesită o intervenție minimă și cu o siguranță ridicată.
Luând în considerare importanța jocurilor video de acest tip prezentate la începutul lucrării, aceast proiect de diplomă aduce un plus industriei, combinând idei noi de funcționalitate pentru aceste jocuri cu modul în care au fost proiectate și implementate.
Prezența pattern-urilor de proiectare impun o claritate în structura codului, delimitând părțile participante și crescând gradul de refolosire și scalare a lor.
6.1 Direcții de dezvoltare
În viitor sunt plănuite acțiuni pentru refactorizarea unor porțiuni de cod, implementarea unor funcționalități noi, cât și optimizarea proceselor existente. O porțiune foarte importantă ce va avea un impact masiv asupra jocului va fi schimbarea completă a jocului din punct de vedere vizual, fiind necesară adăugarea de efecte mai semnificative și a unor imagini de o calitate superioară cu o reprezentare mai concretă.
Din punct de vedere a conținutului se mai pot adăuga tipuri noi de creaturi, cărți noi, personaje noi, moduri noi de joc cât și posibilitatea de a te juca în rețea.
7 Referințe bibliografice
[1] Granic, I., Lobel, A., & Engels, R. C. M. E. (2014), The benefits of playing video games. American Psychologist, 69(1), 66-78.
[2] James, B. (2018), Global games market value rising to $134.9bn in 2018. https://www.gamesindustry.biz/articles/2018-12-18-global-games-market-value-rose-to-usd134-9bn-in-2018
[3] James, B. (2018), The Year In Numbers 2018
https://www.gamesindustry.biz/articles/2018-12-17-gamesindustry-biz-presents-the-year-in-numbers-2018
[4] https://www.statista.com/outlook/14030000/102/card-games/europe
[5] Activision, Blizzard (2018).
https://www.statista.com/statistics/323239/number-gamers-hearthstone-heroes-warcraft-worldwide/
[6] https://thinkgaming.com/app-sales-data/14007/hearthstone-heroes-of-warcraft/
[7] https://steamcharts.com/app/601510
[8] https://unity3d.com/public-relations
[9] John K. Haas (2014), A History of the Unity Game Engine
https://web.wpi.edu/Pubs/E-project/Available/E-project-030614-143124/unrestricted/Haas_IQP_Final.pdf
[10] H. Schildt (2002), C#, Teora, 5
[11] https://www.atlassian.com/software/jira/comparison/jira-vs-trello
[12] https://www.cloudbees.com/jenkins/about
[13] https://www.educba.com/jenkins-vs-circleci/
[14] https://www.quora.com/Which-is-better-for-virtualization-VMware-or-VirtualBox
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: Framework pentru dezvoltarea de aplicații de tip CCG [306686] (ID: 306686)
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.
