Objavljeno: 24.9.2013 | Avtor: Matevž Pesek, Ciril Bohak | Monitor Oktober 2013

Razvoj 3D igre – Asteroidi, 3. del

Grafična podoba igre, ki smo jo razvijali prejšnje mesece, je precej dodelana. V zadnjem članku smo prav tako dodali zmožnost premikov vesoljske ladje, a do prave igre manjka še precej podrobnosti. Tokrat se bomo posvetili izdelavi menija in točkovalnega sistema, ki bo igro naredil zares atraktivno in uporabniku vzpodbudil željo po igranju. Predstavljena vsebina je skupaj s programsko kodo dostopna na android.monitor.si.

S programskim paketom Rajawali smo si že v preteklih delih olajšali izvedbo ključnih gradnikov naše igre. Ogrodje nam je prišlo najbolj prav pri uvažanju modelov, premikih in implementaciji zaznavanja trkov, ki so vsak zase precej težavni. Naslednja stopnica na poti do dokončanja igre je uvedba dodatkov, ki bodo igro naredili privlačnejšo in izoblikovali smiselno celoto.

Glavna želja igralca je dosegati cilje, predstavljene v igri. Pri dvodimenzionalni implementaciji igre Space Invaders smo v ta namen dodali stopnje, ki so s progresivnostjo hitrosti premikanja sovražnikov igro dodatno otežile in popestrile igranje. Točke smo prištevali glede na število uničenih sovražnikov. Tudi pri igri Asteroidi bomo uporabili točkovanje s stopnjami. Točke bomo shranjevali in stopnje oteževali glede na število asteroidov pri vsaki inicializaciji vesolja na začetku stopnje. Kot dodatno težavnost lahko uporabimo tudi hitrost premikanja asteroidov ali pa omejimo premikanje igralčeve vesoljske ladje.

Za hranjenje najboljših rezultatov bomo predstavili lestvico najboljših igralcev »Top Score«, ki jo bomo umestili v razdelek menuja. Sam menu bomo implementirali z namenom predstavitve možnosti igre, kot je možnost igranja nove igre, nadaljevanja nedokončane igre, prikaz najboljših točk vseh igralcev na mobilni napravi ali možnost izhoda. Menu uporabniku prav tako olajša vstop v novo igro, saj jo igralec zažene s pritiskom na gumb, v trenutni fazi razvoja aplikacije pa je prepuščen takojšnjemu začetku igre.

Lestvica najboljših igralcev je tudi del osnovne igre. Z vsakim trkom med igralčevo vesoljsko ladjo in asteroidom je igralec izgubil eno življenje. Ko je življenj zmanjkalo, se je na zaslonu prikazal konec igre (»Game Over«) in vnosno polje za ime, ki se je shranilo na lestvico najboljših igralcev.

Slika 1: Implementacijo osnovne igre v različici za Commodore 64 lahko  odigrate tudi v spletu. Vnos na lestvico najboljših se pokaže po končani igri.

Slika 1: Implementacijo osnovne igre v različici za Commodore 64 lahko odigrate tudi v spletu. Vnos na lestvico najboljših se pokaže po končani igri.

Prikaz točk v igri

Trenutno stanje točk bomo prikazali na vrhu zaslona in rezultat med igro spreminjali glede na število uničenih asteroidov. Za izpis točk definirajmo razred ScoreView, ki razširja TextView. Rezultat bomo izpisali v obliki besedila »Score:« skupaj s številom točk. V spremenljivki score bomo v obliki niza shranili izpis za zaslon. Ob morebitni spremembi točkovanja bomo v spremenljivko shranili novo številsko vrednost v obliki niza. V preobloženi metodi onDraw definirajmo besedilo, ki ga želimo izpisati.

protected void onDraw(Canvas canvas) {

  this.setText(context.getResources().

    getString(R.string.score) + ": " + score);

  super.onDraw(canvas);

}

Niz z besedo »score« smo shranili v xml datoteko strings.xml, ki je del virov, za poznejšo uvedbo večjezične podpore.

V razredu ScoreView je treba omogočiti spremembo besedila ob spremembi točk med igro. Ob morebitni spremembi, ki jo zaznavamo v glavni zanki igre, želimo spremeniti lastnosti več predmetom. Uporabili bomo vmesnik GameStateListener, ki nam bo omogočil proženje sprememb med igro. Vmesnik je zelo preprost, saj vsebuje le metodo za spremembo onValueChanged(String key, String value). Spremenljivka key ponazarja našo identifikacijo poslušalca. Slednjega lahko prikličemo z zgoščene tabele vseh poslušalcev, zato v glavni zanki ni treba ustvarjati lokalne spremenljivke za vsakega poslušalca posebej. Zaenkrat imamo le pet poslušalcev in lahko uporabimo tudi rešitev z ustvarjanjem spremenljivke za vsakega. Kljub temu bomo vpeljali kompleksnejšo različico, saj nam bo prišla prav pri nadaljnjem razvoju igre. Tako bo precej laže dodajati elemente in brati kodo. Razredu ScoreView bomo dodali razširitev prek vmesnika GameStateListener in razred dopolnili z metodo.

public void onValueChanged(String key,

  String value) {

    score = value;

    postInvalidate();

}

Pripravili smo potrebne gradnike za izpis in spremembo točk med igro. Podobno kot v zgledu, ki smo si ga pravkar ogledali, bomo vpeljali tudi druge spremenljive dele igre. Doslej se je igra po nalaganju zagnala brez posebnih opozoril. Prav tako nismo imeli možnosti izbire nove igre in izpisa najboljših igralcev ali izhoda iz igre (izhod iz vsake aplikacije je mogoč s pritiskom ločene tipke nazaj na napravi ali na spodnjem delu zaslona). Igri bomo dodali nov navidezni pogled, ki bo rabil kot vstopni vmesnik - menu igre. Iz pogleda bomo pognali novo igro, si ogledali seznam igralcev, dodali razdelek z opisom igre ali preprosto zaprli aplikacijo. Izris vsakega izmed vmesnikov bomo popestrili s kratko animacijo.

Menu in razdelki

Vstopna točka menuja bo pogled s štirimi gumbi. Uporabnik bo z menuja po želji pognal novo igro, si ogledal najboljše igralce na napravi ali prebral opis igre. Poleg pogledov menuja bomo uvedli tudi pogled, ki bo rabil kot zaslon za čakanje med nalaganjem igre. V trenutni različici se na nekaterih starejših napravah pojavi časovni zamik med zagonom igre in začetkom igranja. Dodali bomo zaslon s sporočilom o vsebini, ki se nalaga. Implementacija zaslona za nalaganje je na voljo v kodi, dosegljivi na spletni strani, v nadaljevanju pa si bomo ogledali implementacijo razdelkov menuja, ki se v grobem ne razlikujejo bistveno.

Menu bomo vpeljali ob pomoči razreda HUD. Za videz, prikazan na sliki 2, je treba ustvariti navpično razporeditev pogleda. Gumbe bomo enostavno dodajali drugega za drugim, pogled pa bo poskrbel za primerno sredinsko razporeditev.

gameMenuLayout = new LinearLayout(context);

...

//vzpostavimo vertikalno razporeditev

gameMenuLayout.setOrientation(LinearLayout.

  VERTICAL);

//nastavimo sredinsko poravnavo

gameMenuLayout.setGravity(Gravity.CENTER);

//določimo vir za ozadje pogleda

gameMenuLayout.setBackgroundResource(R.

  drawable.status_display);

Slika2: Vstopni vmesnik igre. Poleg seznama najboljših igralcev bomo dodali tudi razdelek o igri s kratkim opisom in informacijo o razvijalcih.

Slika2: Vstopni vmesnik igre. Poleg seznama najboljših igralcev bomo dodali tudi razdelek o igri s kratkim opisom in informacijo o razvijalcih.

Podoben pristop bi lahko ubrali tudi prek grafičnega vmesnika za razporejanje elementov ali z urejanjem datoteke xml. Tokrat bomo za spremembo zaradi dodatnih sprememb gumbe napisali kar v programski kodi.

Gumbu bomo dodali velikost in odmik od roba. Za izpis besedila na gumbu bomo znova uporabili nize, ustvarjene v datoteki strings.xml. Niz lahko v programski kodi slogovno uredimo in dodamo vir za ozadje. Zgled si oglejmo na gumbu za novo igro.

newGameButton.setLayoutParams(buttonLayout);

newGameButton.setText(R.string.new_game_en);

newGameButton.setTextAppearance(context,

  R.style.TextWhiteBold);

newGameButton.setBackgroundResource(R.

  drawable.menu_button);

//gumb dodamo v hierarhijo pogleda

gameMenuLayout.addView(newGameButton);

Ne glede na to, ali smo gumb ustvarili v programski kodi ali pa smo za to uporabili poseben pogled in urejanje v datoteki xml, moramo gumbu dodati poslušalca za odziv na pritisk gumba. Ob morebitni novi igri bomo pognali metodo za izvedbo glavne zanke igre.

 public void onClick(View v) {

    gamePlayHUD();

    mRenderer.start();

  }

Vsakemu od preostalih gumbov bomo dodali lastnega poslušalca in ob izvedbi pritiska bodisi prikazali najboljše igralce s klicem metode highScoresHUD, razdelek z opisom igre s klicem metode aboutHUD ali pa bomo iz igre izstopili.

Metode v vsakem izmed zgoraj naštetih zgledov izvedejo menjavo pogleda. Menjava se opravi v trenutku, zato prehod ni ravno gladek in se razlikuje od prehoda na novo dejavnost, ki jo izvede sistem pri zagonu aplikacije. Z animacijami, vgrajenimi v API, lahko prehod naredimo privlačnejši. Prehod na razdelek o najboljših igralcih bomo vpeljali ob pomoči animacije premika trenutnega pogleda navzgor in prikaza razdelka. Uporabimo lahko vgrajeno animacijo vrste TranslateAnimation in v konstruktorju podamo položaj in razdaljo premika.

gameMenuHideAnimation = new TranslateAnimation(0, 0, 0, -height);

Ob morebitnem premiku menuja bomo z animacijo premaknili pogled navzgor za celotno višino zaslona. Na podoben način bomo prikazali nov razdelek z animacijo translacije pogleda za celotno višino zaslona navzgor.

viewShowAnimation = new TranslateAnimation(0,

  0, height, 0);

Slika 3: Ujeti trenutek premika med razdelki. Za prehod smo uporabili vgrajeno možnost animacije prikazanih prvin.

Slika 3: Ujeti trenutek premika med razdelki. Za prehod smo uporabili vgrajeno možnost animacije prikazanih prvin.

Slika 3 prikazuje trenutek med prehodom na nov pogled. Glavni menu se navidezno umika navzgor, obenem pa se izza spodnjega roba zaslona prikazuje tabela najboljših igralcev.

Poslušalcem na pritisk gumbov moramo dodati klic za prehod. Gumbu za izpis najboljših igralcev bomo tako dodali naslednje:

gameMenuLayout.startAnimation(gameMenuHide

  Animation);

highScoresHUD();

hsViewLayout.startAnimation(viewShowAnimation);

Ustrezno bomo priredili tudi druge klice novih pogledov. Vzpostavitev novega pogleda za najboljše igralce in razdelka o igri v metodah highScoresHUD in aboutHUD) je podobna izrisu glavnega menuja.  Pogled postavimo za vertikalno razporeditev, dodamo naslov razdelka in vsebino ob pomoči elementov tipa TextView. V obeh razdelkih je treba dodati gumb za pomik na glavni menu skupaj s poslušalcem za pritisk.

Zbiranje točk in vpis na seznam najboljših igralcev

Za izpis točk smo na uporabniški vmesnik že prej postavili tekstovni element. Točke pa se trenutno še ne osvežujejo. Za štetje točk potrebujemo novo spremenljivko v glavni zanki igre. Stanje spremenljivke bomo osvežili ob vsakem trku izstrelka in asteroida. Novo vrednost moramo tudi zapisati v prikazani ScoreView. Vrednost bomo ob uničenju vesoljske ladjice pri trku z asteroidom primerjali s tabelo najboljših igralcev. Če je uporabnik v igri dosegel primerljivo število točk, bomo njegovo ime zapisali v tabelo, najslabši rezultat pa umaknili iz nje.

Seznam bi lahko naredili tudi poljubno dolg, a se bomo omejili na pet najboljših rezultatov zaradi preglednejšega prikaza rezultatov na menuju. Prav tako bomo rezultate hranili zgolj krajevno – na voljo pa je možnost kasnejše razširitve sistema točkovanja s povezovanjem v spletni strežnik ter shranjevanjem in primerjavo rezultatov med vsemi igralci. Zgled androidne aplikacije, povezljive v splet z našim strežnikom PHP, smo si že ogledali lani [Monitor, oktober 2012].

V razredu AsteroidsRenderer smo do zdaj že uvedli zaznavanje trkov in ob trku med nabojem in asteroidom sprožili ustrezno akcijo. Za vsak asteroid smo preverili morebiten trk s katerim izmed sproženih izstrelkov. Ob trku smo asteroid in izstrelek uničili. V trenutku uničenja asteroida bomo igralca za uspešen zadetek nagradili. Ker smo v aplikacijo dodali razred GameState, prek katerega lahko spreminjamo izpis na komponenti ScoreView, lahko točke zdaj spremenimo s klicem metode put s parametrom ključa, po katerem najdemo pravega poslušalca, in vrednostjo, s katero želimo zamenjati stari izpis.

GameState.put(GameState.KEY_GAME_SCORE,

  GameState.get(GameState.KEY_GAME_SCORE) + 100);

Ob vsakem uspešnem trku bomo igralcu dodelili 100 dodatnih točk. Izpis na zaslonu se bo samodejno spremenil zaradi preobložene metode onDraw v razredu ScoreView, ki smo jo predhodno priredili za spremembo besedila.

Za izdelavo seznama igralcev moramo osvojene točke shraniti tudi krajevno v tabelo. Tabela mora za vsak vnos hraniti vrednosti imena igralca in števila doseženih točk. Želimo pa si tudi, da bi se seznam pri novem zagonu aplikacije ne izpraznil, temveč bi ostal krajevno shranjen v napravi. Shranjevanje in nalaganje podatkov pri odpiranju in zapiranju dejavnosti smo si v preteklih člankih že ogledali. V metodi onCreate lahko prek pridobljenega predmeta Bundle savedInstanceState shranjujemo podatke.

Drug, primernejši način shranjevanja podatkov je prek razreda SharedPreferences. Trivialna rešitev shranjevanja petih najboljših igralcev lahko vsebuje enostavno shranjevanje petih parov imen in doseženih točk.

SharedPreferences.Editor editor =

  sPreferences.edit();

editor.putString("igralec1", "Janko-1000");

...

editor.putString("igralec5", "Metka-100");

editor.commit();

Takšno kodo lahko prilagodimo poljubnemu številu igralcev z uporabo zanke for. Ob zagonu igre lahko naložimo točke s klici shranjenih igralcev. Če vpisa ni, na mesto izpišemo »nezasedeno mesto« in dodelimo 0 točk. Rezultate shranimo v prioritetno vrsto (PriorityQueue) v parih tipa niz in število. Čeprav za izpis točk ne potrebujemo v obliki števila, niz z zapisom števila točk pretvorimo za lažjo primerjavo pri vpeljavi razreda TopIgralec, ki bo služil shranjevanju vrednosti in imena. Razred bomo razširili z vmesnikom Comparable, ki nam bo v pomoč pri implementaciji prioritetne vrste.

Prioritetna vrsta je podatkovna struktura, ki jo uporabljamo skupaj s primerjalnikom (Comparator). Prioriteto določimo bodisi z implementacijo lastnega primerjalnika bodisi z implementacijo primerljivosti v razredu, ki ga primerjamo – v našem primeru razred TopIgralec. Z metodo poll iz vrste izvlečemo prvi element in ga s tem tudi zbrišemo iz nje. Zato bomo pred uporabo tabele za izpis reference (kazalce na primerke) slednje podvojili.

V razredu TopIgralec naj bosta dve javni spremenljivki, ime in tocke. Razredu bomo priredili tudi konstruktor, ki bo vseboval takojšen vnos obeh podatkov – TopIgralec(int tocke, String ime). Primerjanje bomo dodali ob pomoči vmesnika Comparable, ki v razred dodaja metodo compareTo. V slednji v številski obliki z –1, 0 in 1 vrnemo rezultat primerjave. –1 je zgled za to, ko je trenutni primerek večji od drugega, 1 naj pomeni manjši, 0 pa naj izraža enakost. Bolj vešči bralci ste verjetno zasledili podobno, a obrnjeno načelo pri vrednostih večji, manjši. Tokratna implementacija je namenoma prirejena tako, da so večje vrednosti na začetku in manjše na koncu vrste.

Prioritetna vrsta nam bo olajšala primerjavo in izpis ob končani igri ter morebitnem vpisu števila točk na seznam najboljših.

PriorityQueue topIgralci

    = new PriorityQueue();

for(int i = 0; i < 5; i++) {

  String par = sPreferences.getString

    ("uporabnik" + i, "nezasedeno mesto-0");

  TopIgralec igralec = new TopIgralec(par.

    split("-")[0],

    Integer.parseInt(par.split("-")[1]));

  topIgralci.add(igralec);

}

Na drugi strani je ob koncu igre zapis seznama najboljših igralcev v shranjene nastavitve aplikacije prav tako enostaven. Za posamezno vrednost v tabeli igralca shranimo prek metode add, v kateri dodamo nov primerek tipa TopIgralec. Za posameznega igralca zapišemo ime in doseženo število točk. Zaradi narave tabele so rezultati že poprej urejeni, zato se lahko po tabeli le sprehodimo in zapišemo rezultate v urejeni obliki.

Tabelo najboljših igralcev lahko prikažemo kot seznam v razdelku menuja. Prikaz petih igralcev lahko brez skrbi prikažemo na enem zaslonu. Kljub temu bomo upoštevali tudi morebitno željo po povečanju števila zapisov najboljših točk. V vmesnik bomo dodali ScrollView, ki omogoča drsenje po izpisanem seznamu igralcev. Število igralcev z najboljšimi rezultati, ki jih bomo shranjevali, bomo definirali v spremenljivki NUM_TOP_PLAYERS. Spremenljivko lahko nastavimo na poljubno velikost, za praktično uporabo jo bomo nastavili na 50. Prevelikega števila ne priporočamo, saj je treba vse podatke shraniti in naložiti med izvajanjem igre. V razredu HUD bomo v metodi highScoresHUD v zanki sestavili razdelek z najboljšimi igralci.

Iz prioritetne vrste bomo potegnili igralce od najboljšega k najslabšemu. Prioritetno vrsto bomo pred tem podvojili (a le reference in ne dejanskih vrednosti), da ne bi izgubili vnesenih podatkov. V zanki se bomo sprehodili po celotni prioritetni vrsti in za vsakega igralca izpisali ime in dosežene točke.

for (int i = 0; i < topIgralci.size(); i++) {

  TopIgralec t = topIgralci_copy.poll();

  hsOneScoreName = new TextView(context);

... //uredimo izgled polja z nizom

  //dodamo ime

  hsOneScoreName.setText((i+1) + " " + t.ime);

...

  hsOneScoreNumber = new TextView(context);

... //uredimo izgled polja z nizom

  //izpišemo točke

  hsOneScoreNumber.setText("" + t.tocke);

}

Pri izpisu potrebujemo precej več programske kode, da dosežemo želeni videz. Slednjo lahko oblikujete po lastni meri, prikazana koda vsebuje le tiste najpomembnejše dele, ki so potrebni za izgradnjo seznama.

Slika 4: Izpis najboljših igralcev v vrstnem redu od najboljšega k najslabšemu

Slika 4: Izpis najboljših igralcev v vrstnem redu od najboljšega k najslabšemu

Vnos in shranjevanje seznama

Ob končani igri moramo igralčeve točke dodati v prioritetno vrsto. Ker se vrsta ureja po razmerju najvišji proti najnižjemu, lahko vnos doseženih točk trenutne igre enostavno zapišemo kot klic dodajanja rezultata v vrsto.

queue.add(new TopIgralec(

 Integer.parseInt(GameState.get(GameState.

  KEY_GAME_SCORE)),

   "Igralec1"));

Izpis vrste ostaja tak, kot smo ga zapisali v metodi highScoresHUD. Pri vnosu je treba prirediti tudi izbris najslabše prvine, saj želimo ohraniti število mest za najboljše enako. Seznam je tako dopolnjen. Hranil se bo krajevno na napravi, a se bo z morebitno odstranitvijo aplikacije iz naprave seznam zbrisal. Izvedbo on-line shranjevanja najboljših rezultatov zaenkrat prepuščamo bralcu, napotki pa so dosegljivi v projektu “kaj dogaja”, ki je dosegljiv na android.monitor.si.

Nadaljevanje razvoja

Tokrat smo uspešno implementirali metode za prikaz in točkovanje napredka igralca. Točke smo tudi shranili in jih ob novem zagonu igre naložili. Za potrebe hranjenja in razvrščanja smo si ogledali podatkovno strukturo prioritetna vrsta. Igra dobiva končno obliko, a nam zmožnosti mobilnih naprav dopuščajo še precej prostora za izboljšave. V prihodnje si bomo ogledali dopolnitev igre z dodatkom zvočnih in vizualnih učinkov. Eksplozije bo spremljal zvok (čeprav vemo, da se v vesolju ne širi tako kot v hollywoodskih nanizankah), a nam bo znanje prišlo prav pri razvoju drugih iger, kjer je zvok bolj smiseln in znanstveno utemeljen. V prihodnje si bomo ogledali tudi implementacijo eksplozije pri trku asteroida z izstrelkom vesoljske ladje, saj bo potem igra še privlačnejša.

Naroči se na redna tedenska ali mesečna obvestila o novih prispevkih na naši spletni strani!

Komentirajo lahko le prijavljeni uporabniki

 
  • Polja označena z * je potrebno obvezno izpolniti
  • Pošlji