Objavljeno: 4.9.2012 09:02 | Avtor: Matevž Pesek | Monitor Julij-avgust 2012 | Teme: android

Sam svoj programer

V petem delu bomo dodelali igralno zanimivejše lastnosti igre. Spoznali se bomo s konceptom ravni v igri, ki stopnjujejo tempo glede na napredek igralca. Nasprotniki bodo pridobili možnost streljanja, kar bo igro dodatno popestrilo. Tako kot v prvotni igri bo tudi naš končni izdelek omogočal tri življenja igralca. V članku se bomo sprehodili med ključnimi deli kode, potrebnimi za izvedbo opisanih lastnosti. Programska koda, opisana v članku, je skupaj s preteklimi članki iz serije in dodatnimi pojasnili objavljena na android.monitor.si.

Prejšnji mesec smo modeliranje zaslona ponesli na malce težje področje. Spozneli smo se s konceptom glavne zanke igre in ključnih komponent, za katere moramo skrbeti med izvajanjem. Izgradnja osnovne zanke in predstavitev časovnikov (timer) nam je omogočila izvajanje preprostih animacij z uporabo slik. Igra je zdaj osnovno ogrodje za dodajanje novih zmožnosti. V članku tokrat predstavljamo nekatere razširitve, ki dajo igralcu občutek nostalgije, saj aplikacija že v polnosti spominja na prvotno igro.

Razhroščevanje

Z vse večjo zahtevnostjo programiranja aplikacije se veča tudi možnost napak, skritih globoko v aplikaciji. Z zaporednim izvajanjem določenih delov kode na enem mestu v aplikaciji podajamo in shranjujmo informacije, ki so lahko ob uporabi na drugem mestu v aplikaciji že zastarele ali celo neveljavne. Takšno delo s podatki je pogosto tvegano in zahteva premislek, saj se lahko pogosto pojavijo programski hrošči, ki jih nevede vnesemo med programiranjem. Programer takih težav pogosto ne opazi, saj sledi osnovnemu toku dogodkov v igri tako, kot si je zamislil. Ko pa mobilno napravo podamo prijatelju, aplikacija zaide v čudna stanja, od neodzivnosti pa do nenadne zaustavitve (crash). Tokrat bomo pri programiranju še posebej pozorni, da bo v kodi kar najmanj hroščev.

Eden izmed preprostejših načinov odkrivanja hroščev je preprost izpis informacij v konzolo v programskem okolju Eclipse. Kot zgled vzemimo izstrelek vesoljske ladjice. V nekem trenutku dvomimo o pravilnosti izračuna koordinat pri premiku, zato lahko enostavno v metodo move() razreda Bullet dodamo izpis koordinat v konzolo na sledeči način:

public void move() {
float zacasniIzpis = yPos;
yPos -= speed * direction;
System.out.println("HROSC - stari y: " +
zacasniIzpis + " novi y: " + yPos);
}

V zavihku LogCat si lahko ogledamo tak izpis na preprost način med delovanjem aplikacije, pri čemer mora biti naprava s kablom povezana na računalnik. Na desni Izberemo zavihek "All messages". Sporočila naprave se prikažejo v osrednjem delu okna. V vrstico nad sporočili lahko vpišemo ključno besedo "HROSC", ki smo jo uporabili pri izpisu. Programsko okolje uporabi vrednosti v vrstici v obliki filtra ter izpusti vsa sporočila, v katerih ni ključne besede. Med poskusnim igranjem igre lahko v konzoli v realnem času pregledujemo izračun koordinat in preverjamo pravilnost izračuna.

Pri kompleksnejših aplikacijah želimo imeti več različnih izpisov. Na voljo je standarden način izpisovanja pri razhroščevanju z uporabo razreda Log. Razred vsebuje več različnih metod s kratkimi imeni, pomembnejše pa so d - razhroščevanje (angl. debug), v - odrazložitev (angl. verbose), w - opozorilo (angl. warning) in e - napaka (angl. error). V skladu z metodami je moč v zavihku LogCat ustvariti filtre glede na tipe izpisa. Poleg tipa lahko izpisu določimo oznako, ki bo v našem primeru beseda "HROSC".

V metodi move bomo dodali še ustrezen način izpis besedila.

Log.d("HROSC", "trenutni y:" + yPos);

Zavihek LogCat na levi strani prikazuje filtre, med njimi tudi naš filter "Hrosc", ki je označen. Vsebina, vidna na desni, zadošča pogojem filtra.

Nivoji igre

Tokrat bomo dodali lastnosti syopenj igre, kjer bomo postopoma povečevali hitrost premikanja napadalcev. V razred GameLevel bomo dodali spremenljivko private int level, v kateri bomo hranili trenutno stopnjo igre.

Dodali bomo tudi metodo getLevel, ki vrne številko stopnje, njen rezultat pa bomo uporabili pri izrisu grafičnega vmesnika, definiranega v razredu GUI, kjer bomo trenutno stopnjo izpisali na zaslon poleg doseženih točk. Kot smo omenili, želimo z vsako stopnjo malce pohitriti igro in tako otežiti igranje. Zato dodajmo tudi metodo getSpeedFactor, katere rezultat se glede na naraščajočo stopnjo zmanjšuje. Vrednost bomo v razredu invaderTimer pomnožili s številom milisekund, ki določajo interval proženja dogodka premika napadalcev.

public synchronized float getSpeedFactor() {
return 1.0f/(1 + level/10.0f);
}

Matematike vešči bralci v funkciji za izračun faktorja hitro prepoznate funkcijo 1/x. V našem primeru se vrednosti manjšajo malce počasneje, da stopnje za igralca ne postanejo prehitro pretežke.

Seveda moramo stopnjo povečati vsakič, ko uničimo vse napadalce. Sprva bomo dodali novo stanje igre NEW_LEVEL_STATE, ki bo ponazarjalo vmesno stanje po pravkar končani stopnji. V metodi invaderCollisionDetection bomo spremenili zgled, ko je naše polje napadalcev prazno.

invaders.remove(i);
if (invaders.size() == 0) {
currentGameState = NEW_LEVEL_STATE;
}

V tem stanju moramo poskrbeti za več stvari. Treba je ustaviti oziroma preskočiti celotno zanko igre, ki skrbi za premike. V metodi gameLoop bomo na začetek vstavili preverjanje, ko je igra v stanju nove stopnje.

if(currentGameState == NEW_LEVEL_STATE) return;

Tako bomo poskrbeli, da v stanju priprave na novo stopnjo zanka ne spremeni stanja v konec igre. Nadaljevali bomo v metodi render razreda GUI. Na tem mestu bomo v switch stavek dodali preverjanje za primer, ko je igra v stanju NEW_LEVEL_STATE. V tem stanju bomo podobno kot pri koncu igre izrisali sliko z napisom, imenovano new_level.png, ki je bila dodana v vire projekta.

...

case Game.NEW_LEVEL_STATE:
c.drawRect(new RectF(0, 0, width, height), transPaint);
xP = width/2 - (newLevelImage.getWidth() * scale / 2);
yP = height/2 - (newLevelImage.getHeight() * scale / 2);
xS = width/2 + (newLevelImage.getWidth() * scale / 2);
yS = height/2 + (newLevelImage.getHeight() * scale / 2);
c.drawBitmap(newLevelImage, null, new RectF(xP, yP, xS, yS), mPaint);
break;

Spremenljivko newLevelImage moramo seveda ustrezno inicializirati na enak način, kot smo storili s sliko game_over.png. Ob zagonu aplikacije bomo ugotovili, da pri uspešno končani stopnji preidemo v stanje nove stopnje, a se aplikacija neha odzivati na naše dotike. (Ne)odziv aplikacije je po svoje pravilen, saj v tem stanju nismo implementirali dela kode, ki bi poskrbel za izris nove stopnje po interakciji z zaslonom.

Med slikovne vire projekta smo dodali prikaz obvestila o naslednji stopnji. Ob dotiku zaslona aplikacija začne izvajati novo, težjo stopnjo.

V našem razredu poslušalca dotikov OnTouchListener za primer dotika (MotionEvent.ACTION_DOWN) dodajmo preverjanje trenutnega stanja igre za novo stopnjo. Če je igra v stanju priprave na novo stopnjo, lahko enostavno pokličemo metodo startGame in poskrbimo za nov izris napadalcev, kot je prikazano v kodi spodaj:

case NEW_LEVEL_STATE:
startGame();
break;

Pred tem bi bilo dobro povečati stopnjo v razredu gameLevel in seveda prekiniti časovnik invaderTimer. Če tega ne storimo, bomo s klicem metode startGame spet ustvarili nov časovnik, ki bo prav tako premikal napadalce. Obenem bo na prejšnji stopnji ustvarjeni časovni opomnik deloval enako. V praksi lahko ob testiranju aplikacije opazimo nenadzorovane prehitre premike napadalcev. Zato bomo napisali še eno metodo: prepareNextLevel. V njej bomo sprva povečali stopnjo igre v objektu gameLevel, nato bomo zaustavili časovni opomnik, ki premika napadalce, in šele nato klicali metodo startGame.

public void prepareNextLevel() {
gameLevel.increment();
invaderTimer.cancel();
invaderTimer = null;
startGame();
}

Opravili smo pregled implementacije stopenj. Posebna pozornost je potrebna pri implementaciji metod increment, getSpeedFactor in getLevel objekta gameLevel. Vse metode so sinhronizirane, saj se tako znebimo hroščev, ki bi občasno pomešali kakšno številko stopnje.

Prikaz števila točk, skupaj z izpisom številke stopnje.

Streli nasprotnikov

V androidni projekt prejšnjega članka smo dodali sličici bullet1.png ter bullet1a.png, ki vsebujeta prikaz premikajočega izstrelka nasprotnika. Kratek pregled potrebnih implementacij kaže, da lahko večji del kode izstrelka vesoljske ladjice uporabimo pri implementaciji nasprotnikovih strelov. Dodali bomo novo spremenljivko invaderBullet in implementirali metodi fireInvaderBullet, moveInvaderBullet.

Omenimo le nekaj krajših težav pri implementaciji metod. Glede na moveShipBullet, ki je ekvivalentna metoda za premik izstrelka vesoljske ladjice, moramo preverjati veljavnost premika izstrelka in ga ob pozitivnem rezultatu preverjanja tudi premakniti. Metodo razreda Bullet isValid bomo priredili tako, da se v primeru, ko gre za izstrelek ladjice (tip izstrelka - type = 0), izvede že napisana koda, drugače pa se preverja spodnja meja zaslona, saj nasprotnik strelja v nasprotno smer.

public boolean isValid() {
if (type == 0 && yPos > 10)
return true;
else if(type == 1 && yPos < windowHeight)
return true;
return false;
}

Spremenljivko tipa windowHeight moramo ob ustvarjanju vsakega novega primerka objekta tipa Bullet nastaviti na trenutno velikost zaslona. Bolj poglobljena analiza problema pokaže, da se lahko tudi višina zaslona spreminja (uporabnik obrne napravo), a bomo ta primer zaradi težavnosti izpustili. Problemu primerno bomo dodali lokalno spremenljivko float windowHeight, kateri vrednost nastavimo v konstruktorju z dodanim argumentom glede velikosti zaslona.

Prav tako bomo definirali premikanje izstrelka v metodi move.

int factor = -1;
if(type == 1) { //če je izstrelek napadalca
factor = 1;
yPos += factor * speed * direction;
}

Koda vsebuje novo spremenljivko faktor, ki določa smer premika izstrelka bodisi proti ladjici ali proti nasprotniku.

Dopisati moramo že prej omenjeno metodo fireInvaderBullet, ki ustvari nov objekt in nastavi privzete vrednosti.

invaderBullet = new Bullet(gameView.getResources(), gameView.getHeight());
synchronized (invaderBullet) {
invaderBullet.setScale(scale);
invaderBullet.setType(1);
invaderBullet.setPosition(posX, posY);
}

V konstruktorju novega izstrelka dodamo višino zaslona, ki ga vsebuje naš videz v spremenljivki gameView.

Tokrat ne smemo preverjati morebitnega trčenja zadetka z drugimi napadalci, temveč z ladjico. V ta namen spišemo (kot model vzemimo shipCollisionDetection) invaderCollisionDetection, ki preverja morebitni trk nasprotnikovega izstrelka z vesoljsko ladjico. V primeru trka lahko implementiramo konec igre. Pri izračunu trka privzamemo trenutni položaj ladjice in samo velikost prek metod getMinX, getMinY, getMaxX, getMaxY.

if (AABB(ship.getMinX(), ship.getMinY(), ship.getMaxX(), ship.getMaxY(),
invaderBullet.getMinX(), invaderBullet.getMinY(), invaderBullet.getMaxX(),
invaderBullet.getMaxY())) {
currentGameState = GAME_OVER_STATE;
invaderBullet = null;
}

Napadalčev izstrelek moramo v primeru zadetka izbrisati (postaviti na null).

Dodatna značilnost premika izstrelka je pripravljena za uporabo. Implementirati moramo klicanje metode fireInvaderBullet, ki jo proži naključni napadalec. Izbrali smo si enostavnejšo različico implementacije, ki bo naključno izbrala napadalca. Napadalčev položaj naj bo hkrati začetni položaj izstrelka, ki je usmerjen proti vesoljski ladjici.

V metodi gameLoop bomo dodali generator naključnih števil ob pomoči razreda Random.

Ker ne želimo neprestanih, temveč naključne izstrelke napadalcev, bomo sprva ustvarili naključno število med 0 in 1 ter predpostavili, da želimo ustvariti izstrelek le, ko je naključno število v zgornji polovici intervala (večje od 0,5). Kot drug pogoj bomo predpostavili, da naj strelja le en napadalec naenkrat.

Random r = new Random();
if(r.nextDouble() > 0.5 && invaderBullet == null) //ustvari izstrek

Nato bomo izbrali naključnega napadalca med vsemi, ki so v polju napadalcev invaders. Generator števil omogoča uporabo metode nextInt(int n), pri kateri z argumentom n določimo najvišjo mejo intervala celih števil, iz katerega želimo ustvariti naključno število. Ko je napadalec izbran, ustvarimo izstrelek. Koordinate izbranega napadalca nam rabijo kot začetni položaj izstrelka.

SpaceInvader invader = invaders.get(r.nextInt(invaders.size()));
fireInvaderBullet(invader.getX(), invader.getY());

Napadalci že streljajo! Na žalost pa se premiki izstrelka ne izvajajo pravilno. Preden poženemo aplikacijo, moramo metodo moveInvaderBullet sprožiti ob pomoči časovnika bulletTimer. Ker predvidevamo, da je hitrost napadalčevih izstrelkov enaka hitrosti izstrelkov ladjice, ni treba dodajati dodatnega časovnega opomnika.

V metodo run torej dodamo klic metode moveInvaderBullet.

Naključni nasprotnik strelja proti vesoljski ladjici. Izstrelke nasprotnikov izrisujemo z izmenjavo dveh sličic, zaradi česar dobimo učinek animacije valovitega izstrelka.

Življenja

Koncept življenj je v osnovni igri "Space Invaders" podal igralcu možnost nadaljevanja igre brez izgube točk v primeru zadetka vesoljske ladjice s strani napadalcev. Tudi mi bomo implementirali življenja igralca na enak način. V prej omenjeni metodi invaderCollisionDetection bomo v primeru uspešnega zadetka vesoljske ladjice odšteli življenja in rezultat osvežili na zaslonu. Če je število življenj enako nič, je igre konec.

lives--;
gui.setLives(lives); //Vmesniku določimo novo število življenj
if(lives == 0){ //ni več življenj
currentGameState = GAME_OVER_STATE;
}

Izpis življenj na zaslonu bomo dodali k izpisu točk.

Ob pomoči zanke, ki izrisuje posamezne števke iz niza, ki predstavlja število točk, bomo izrisali še številko stopnje, na kateri je igralec.

String scoreLevel = score + " " + level;

Problem seveda nastane pri metodi Integer.parseInt, ki poskusi neuspešno prevesti presledek v številko. Zato bomo metodo ovili v try-catch blok, ki omogoča lovljenje izjem. Elegantnejša možnost bi bila seveda ločen izris številke stopnje neodvisno od števila točk, prej omenjena možnost pa je hitreje na voljo, saj smo prevedbo števil v slike števk že implementirali. Poleg števila točk se na zaslonu prikaže število stopenj, od točk ločeno s presledkom, ki je po razdalji enak eni širini posamezne števke. Takšno rešitev smo uporabili zaradi pomanjkanja prostora na manjših napravah, bralec pa lahko uporabi prej omenjeno ločeno rešitev skupaj z opisom, da število ponazarja trenutno številko stopnje, ki jo je igralec dosegel.

Vse implementirane zmožnosti igre. Število življenj se po uspešno končani stopnji spet nastavi na tri.

Sklep

Serija člankov o programiranju za operacijski sistem Android se s tem člankom končuje. V prvih delih smo se spoznavali s programskim okoljem, orodji in knjižnicami, ki jih je Google pripravil za lažje programiranje, ogledali smo si tudi nekaj osnov programskega jezika java. S preprostimi zgledi izrisa in premikanja slik po zaslonu ter skozi nadgradnjo zgledov v enostavno galerijo smo dosegli nov mejnik. Implementirali smo osnovno zanko igre, dodali premike ključnih elementov igre in se dotaknili ključnih problemov, s katerimi se spoprime vsak začetnik ne glede na vsebino igre, ki jo želi implementirati. Nekatere probleme smo rešili enostavno, drugi pa v tem trenutku našega razvoja igre ostajajo nerešeni. Velik poudarek smo namenili predvsem programiranju enostavnih in naprednejših aplikacij, marsikdo pa je verjetno dobil tudi lastne zamisli o novih aplikacijah in razširitvah predstavljenih. Najpomembnejša je seveda zamisel, s kančkom volje, z željo po uspehu, s pomočjo trgovine spletnih aplikacij Google Play pa lahko tudi povprečen uporabnik ustvari in objavi aplikacijo v širni svet na preprost način.

Pomemben del aplikacij, ki so zelo povezane z animacijami, kot na primer igre, uporablja drugačne pristope. Glavnina predstavljene igre vsebuje premike slik glede na časovni interval. V naši seriji se izdelave 3D iger nismo dotaknili, saj zahteva takšno programiranje precej predznanja in še več matematične podlage. V spletu je moč najti več dobrih vodnikov tako za osnovno kot tudi nadaljevalno stopnjo razvoja aplikacij. Ker je za marsikoga učenje v drugem jeziku ovira, so vsebine serije člankov z dodatnimi obrazložitvami in zgledi dostopne v spletu.

Avtorja se bralcem zahvaljujeva za izkazano pozornost z obiskom spletne strani in upava, da vaše navdušenje do programiranja ne bo usahnilo s koncem serije. Zanimive izpeljave igre, dodatke in predstavitve novih zmožnosti igranja lahko objavite na dverih.

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