Sam svoj programer
V tretjem delu niza člankov "Programiranje za Android" bomo z izdelavo galerije slik za korak bliže končni aplikaciji. V prvem članku smo si ogledali, kako vzpostaviti razvojno okolje, v drugem smo izvedeli malo več o okolju Android in si ogledali premik slike glede na dotik prsta na zaslonu, tokrat pa bomo dotik prsta uporabili za premikanje slik v dveh ločenih mini galerijah. V galeriji bomo dodali slike napadalcev in vesoljske ladjice. Pridobljeno znanje bomo kasneje uporabili pri implementaciji premikov naše vesoljske ladjice v igri "Space Invaders". Pretekle članke iz serije si, skupaj s kodo in dodatnimi obrazložitvami, lahko ogledate na android.monitor.si.
Kot je bilo predstavljeno v prejšnjem delu, je lahko aktivnost v različnih stanjih, med katerimi lahko med izvajanjem prehaja. Na tem mestu lahko naletimo na težave shranjevanja stanja aktivnosti. Dokler je aktivnost v ospredju, oz. je še na skladu izvajajočih se aktivnosti, shranjevanje stanja ni potrebno, saj za to poskrbi sam sistem. Že pri enostavnih akcijah pa naletimo na situacije, ko želimo stanje aktivnosti shraniti, da ga ne bi izgubili. Najbolj enostaven primer je sprememba orientacije zaslona aktivnosti.
Obnavljanje shranjenega stanja se izvede vsakič že ob klicu metode onCreate, ki sprejme atribut tipa Bundle. Ta objekt hrani podatke o stanju obliki parov <ime, vrednost>. Morebitno shranjevanje stanja je treba izvesti pri klicu metode onSaveInstanceState. Klic te metode sproži sistem pred klicem metode onPause. Metoda onSaveInstanceState pa ni ena izmed metod, ki se kličejo med prehodi, saj se kliče le v določenih primerih. Tako je treba, če pomembnih podatkov nikakor ne želimo izgubiti, zanje poskrbeti in jih trajno shraniti (npr. na pomnilniško kartico ali v pomnilnik naprave) znotraj metode onPause. Na opisani način lahko podatke shranjujemo le v obliki nizov. V tem delu uporaba shranjevanja stanja ne bo glavna tema, kljub temu pa bomo naleteli na situacijo, ko bo takšna uporaba v prihodnosti smiselna.
Razvoj tretje aplikacije
Premik slike na mesto dotika nas je popeljal v osnove interakcije uporabnika z našim programom. Sličico vesoljskega napadalca smo uspešno premikali po zaslonu, seveda z malce goljufije, saj se je napadalec v primeru pritiska na zaslon premaknil kar na položaj prsta. V nadaljevanju želimo našo ladjico premikati glede na položaj prsta na način, da upoštevamo pritisk prsta na zaslonu, pa tudi trenutni položaj vesoljske ladjice. Seveda to z manjšo pomočjo matematičnega znanja ne bo hud zalogaj. Problem bomo rešili skozi projekt mini galerije, ki bo namenjena prikazu slik na zaslonu, in interakcijo s pomočjo dotika na zaslonu. Slike v galeriji ne bodo kar skočile na položaj dotika prsta, temveč bodo navidezno drsele v vodoravni smeri premika prsta po zaslonu.
Starejši bralci, ki se še spominjate legendarne igre "Space Invaders", veste, da igra temelji na uspešnem premikanju vesoljske ladjice med levim in desnim robom zaslona. Vesoljska ladjica je s svojim uničevanjem sovražnikov varovala naš planet pred invazijo. Sovražniki se vztrajno premikajo proti spodnjemu robu zaslona in poskušajo zavzeti planet. Premiki sovražnikov po zaslonu so trenutno še malce težji problem (odvisni so od odločitev računalnika), vesoljska ladjica pa je prepuščena nam samim. Ker večina telefonov nima smernih tipk (razen nekaterih, ki imajo celotno fizično tipkovnico), se bo naša priredba igre odzivala na dotike zaslona. Vesoljska ladjica bo tako drsela bodisi levo ali desno znotraj meja našega zaslona.
Zaslon bomo navidezno razdelili na dvoje. V zgornjem delu zaslona bomo pripravili mini "galerijo" med seboj enakih sličic napadalcev, v spodnjem delu pa bomo postopek ponovili, pri čemer bomo prikazovali le eno sličico, ki bo predstavljala našo vesoljsko ladjico. Ker se želimo premikati po obeh "galerijah" z drsenjem prsta, bo implementacija vsebovala del kode, ki smo jo napisali že v predhodnem članku.
Začeli bomo s projektom iz prejšnjega članka. Projekt Eclipse najdete na spletni strani v obliki arhivskega paketa (.zip). Z uspešno priredbo kode bomo prikazali več napadalcev in našo ladjico. V razredu GameView bomo delu spremenljivk spremenili ime in dodali nekaj novih. Potrebovali bomo sliko napadalca, ki jo bomo shranili v spremenljivko invaderImage, prav tako bomo pripravili spremenljivko istega tipa Bitmap za sličico naše vesoljske ladjice, playerImage.
V obliki števila moramo pomniti tudi, koliko enakih slik napadalcev želimo prikazati na zgornjem delu zaslona. Število slik, ki jih želimo prikazati, bomo hranili v spremenljivki numRanges. Prav tako bomo hranili tudi položaj posameznega napadalca. Vse položaje napadalcev, ne glede na število, lahko izrazimo tudi v obliki položaja najbolj levega prikazanega napadalca in odmika od njega. Tako bomo za shranjevanje položaja napadalca v vodoravni smeri uporabili spremenljivko invaderPositionX. Spremenljivka invaderOffsetX bo rabila za hranjenje odmika med posameznima napadalcema. V navpični smeri bomo odmik hranili v spremenljivki invaderOffsetY in se med izvajanjem aplikacije ne bo spreminjal. Za navpični odmik bomo določili 50 slikovnih pik, da bo galerija napadalcev primerno odmaknjena od zgornjega roba zaslona. S podobnim namenom bomo uporabili spremenljivko playerOffsetY, ki bo določala odmik igralčeve vesoljske ladjice od spodnjega roba zaslona.
Preglejmo še metode, ki so že vsebovane v našem projektu. Metoda initGameView bo tokrat vsebovala dve inicializaciji spremenljivk poleg inicializacije razreda mPaint. V spremenljivko invaderImage bomo shranili sliko napadalca (invader1.png), ki jo že imamo kot del virov v našem projektu. V playerImage bomo naložili novo sliko (ship.png), ki vsebuje našo vesoljsko ladjico.
invaderImage = BitmapFactory.decodeResource(getResources(), R.drawable.invader1);
playerImage = BitmapFactory.decodeResource(getResources(), R.drawable.ship);
Sliki sta naloženi, spremeniti moramo še metodi onDraw in setPosition. Na zgornjem delu zaslona želimo izrisati več napadalcev, na spodnjem delu zaslona pa našo ladjico. Ladjico bomo izrisali z uporabo risalnega platna (canvas) oz., natančneje, z njegovo metodo drawBitmap.
canvas.drawBitmap(playerImage, playerPositionX, playerOffsetY, mPaint);
Za vodoravni položaj bomo upoštevali spremenljivko playerPositionX, odmik od spodnjega roba bo enak playerOffsetY, pri čemer moramo upoštevati tudi samo velikost zaslona. Slednjo pridobimo s klicem metode našega pogleda getHeight. Tako moramo določiti playerOffsetY kot razliko med višino zaslona in neko vnaprej določeno vrednostjo odmika, v našem primeru 200 slikovnih pik. Pred izrisom bomo zato izračunali navpični položaj slike naše ladjice.
playerOffsetY = this.getHeight()-200;
Izrišimo še slike napadalcev na zgornjem delu zaslona. Za izris enega napadalca bomo uporabili enak pristop kot pri izrisu igralčeve ladjice, posebno pozornost moramo posvetiti izrisu več napadalcev. Uporabili bomo zanko for, ki bo izrisala vse napadalce, pri čemer bomo izračunali, za koliko moramo položaj posameznega napadalca premakniti v desno glede na najbolj levega napadalca. Prvi napadalec bo nahajal v vodoravnem položaju, določenem s spremenljivko invaderPositionX, njegov navpični položaj pa je določen z vrednostjo, shranjeno v spremenljivki invaderOffsetY.
Izračunati je treba tudi odmik med parom slik napadalcev. Upoštevati je treba širino zaslona in število napadalcev, pozabiti ne smemo niti na širino posamezne slike napadalca.
invaderOffsetX = this.getWidth() / numInvaders;
Nato ob pomoči zanke for izrišemo posameznega napadalca.
for(int i = 0; i < numInvaders; i++) {
canvas.drawBitmap(invaderImage, invaderPositionX + i*invaderOffsetX, invaderOffsetY, mPaint);
}
Tako, aplikacija je skoraj pripravljena. Spremeniti moramo še metodo setPosition, ki bo tokrat drugače obravnavala začetek dotika prsta (down) in premik prsta (move). Zato bomo metodi dodali parameter saveOnly, ki bo omogočal shranjevanje prvotnega položaja prsta ob pritisku na zaslon. Pri premiku prsta po zaslonu želimo sliko v galeriji premakniti levo ali desno za razdaljo, enako premiku prsta v vodoravni smeri zaslona. Zato moramo najprej shraniti vrednost začetnega položaja prsta na zaslonu, nato pa preračunavati relativen premik prsta glede na zadnjo lokacijo pred premikom.
Seveda želimo galerijo napadalcev na vrhu zaslona premikati ločeno od vesoljske ladjice. Zato bomo zaslon navidezno ločili na zgornjo in spodnjo polovico, tako da bomo preverjali vertikalno lokacijo prsta na zaslonu. Če bo šlo za pritisk v zgornji polovici zaslona, bomo slednje upoštevali pri premiku galerije napadalcev, drugače bo gesta vplivala na premik vesoljske ladjice.
Metoda bo videti takole:
public void setPosition(float x, float y, boolean saveOnly) {
//preverimo y - ali zelimo premakniti igralcevo ladjico ali napadalce
if(y > this.getHeight()/2) {
if(saveOnly) {
playerPrevPositionX = x;
return;
}
float delta = x - playerPrevPositionX;
playerPositionX = playerPositionX + delta;
playerPrevPositionX = x;
} else {
if(saveOnly) {
invaderPrevPositionX = x;
return;
}
float delta = x-invaderPrevPositionX;
invaderPositionX = invaderPositionX + delta;
invaderPrevPositionX = x;
}
}
Očitno je, da je koda za premik napadalcev in ladjice precej podobna, razen imen spremenljivk, saj gesta enkrat vpliva na zgornji del galerije napadalcev, drugič pa na vesoljsko ladjico. V vsakem primeru najprej pogledamo, ali želimo le shraniti prvi dotik prsta, sicer pa izračunamo relativen odmik prsta glede na prejšnji dogodek premika in to upoštevamo pri premiku napadalca oziroma ladjice.
Trenutna podoba naše aplikacije, ki se odziva na premike prstov po zaslonu.
Projekt že deluje, a še ni končan. Slika 1 prikazuje naš test na androidni tablici. Pri testu na tablici ali v simulatorju opazimo, da galerija deluje neintuitivno, saj lahko vse sličice napadalcev skrijemo za levi ali desni rob zaslona. Ker se ponavadi galerije slik pomikajo ciklično, želimo sliko, ki se skrije za levi del zaslona, izrisati na desni strani. To bomo storili tako, da bomo dodali izris iste slike na drugem položaju tako pri napadalcih kot pri vesoljski ladjici. Te slike bodo zamaknjene za celotno širino zaslona proti levi. Uporabili bomo tudi deljenje po modulu širine zaslona. Kaj to pomeni v praksi? Poleg sličice, ki se, recimo, prikaže na sredini zaslona, bomo izrisali tudi sličico, zamaknjeno za celotno širino zaslona v levo. Ta sličica ne bo vidna, dokler bo osnovna sličica na zaslonu. Ko bo osnovna sličica segala čez desni rob zaslona, se bo na levi strani prikazala prej skrita druga sličica. Občutek bo za uporabnika približno tak, kot da bi se del slike izza desnega roba zaslona preslikal na levi del.
canvas.drawBitmap(invaderImage, (invaderPositionX - getWidth() + i*invaderOffsetX) % this.getWidth(), invaderOffsetY, mPaint);
Popravljena podoba aplikacije, ki upošteva prekrivanje slike za robom in preostanek navidezno preslika na drugo stran zaslona.
Vrstico dodamo v for zanko za izris sovražnikov. Gre za vrstico, enako tisti, ki je že v zanki, s tem da pri x koordinati odštejemo širino zaslona. Enako ponovimo tudi pri izrisu vesoljske ladjice. Pri tem ne pozabimo položaja sovražnika in ladjice deliti po modulu širine zaslona, kar dodamo na konec metode onDraw.
invaderPositionX = invaderPositionX % this.getWidth();
playerPositionX = playerPositionX % this.getWidth();
Pri vnovičnem zagonu aplikacije opazimo, da se galerija ciklično ponavlja od enega roba proti drugemu, glede na smer drsenja prsta (ali miške) po napravi.
Da bi aplikacijo naredili še malo bolj podobno igri, dodajmo še metodo destroyInvader, ki bo ob pritisku na našo vesoljsko ladjico število napadalcev v zgornji galeriji zmanjšala za ena, dokler ne ostane en sam napadalec.
V metodi bomo sprva preverili, ali je število napadalcev večje od ena, nato pa bomo primerjali x in y položaj prsta s položajem naše vesoljske ladjice na zaslonu. Če je bil premik ali "klik" s prstom izveden znotraj robov slike vesoljske ladjice, bomo število napadalcev preko spremenljivke numInvaders zmanjšali za ena. To bo na zaslonu vidno ob naslednjem izrisu (klicu metode onDraw).
public void destoryInvader(float x, float y) {
if(numInvaders > 1) {
if(x > playerPositionX && x < playerPositionX + playerImage.getWidth()) {
if(y > playerOffsetY && x < playerOffsetY + playerImage.getHeight()) {
numInvaders--;
}
}
}
}
Lokacijo prsta smo v metodi preverili tako, da mora biti posamezna koordinata večja od levega oz. zgornjega položaja roba slike in manjša od desnega oz. spodnjega položaja roba.
Z vpeljavo sprememb in novih metod razreda GameView smo končali. Oglejmo si še, kaj bomo spremenili v glavni aktivnosti And_TouchInvaderActivity. Metodo onTouch našega poslušalca touchListener bomo spremenili tako, da bomo v primeru dogodka MotionEvent.ACTION_DOWN klicali metodo setPosition razreda GameView s tretjim parametrom, nastavljenim na true, saj gre za prvi dotik s prstom. V primeru dogodka MotionEvent.ACTION_MOVE bomo setPosition klicali s parametrom, nastavljenim na false, saj gre za premik in ne za prvi dotik zaslona. V primeru MotionEvent.ACTION_UP pa bomo gesto upoštevali kot klik zaslona in bomo klicali metodo destroyInvader. Ta metoda bo preverila, ali je šlo za klik naše vesoljske ladjice, in v tem primeru zmanjšala število izrisanih napadalcev.
Switch stavek bo videti tako:
switch (action) {
case MotionEvent.ACTION_DOWN:
((GameView)v).setPosition(event.getX(), event.getY(), true);
v.postInvalidate(); //gre za prvi dotik zaslona
case MotionEvent.ACTION_MOVE:
((GameView)v).setPosition(event.getX(), event.getY(),false);
v.postInvalidate(); //gre za premik po zaslonu
break;
case MotionEvent.ACTION_UP:
((GameView)v).destoryInvader(event.getX(), event.getY());
break; //gre za klik na zaslon
}
Dobro je vedeti, da bi morali dejansko ločiti akcije uničenja napadalca in premika tako, da bi pri premiku kot doslej upoštevali le akcijo MOVE, "klik" zaslona pa bi bil sestavljen iz akcije DOWN in akcije UP (dotik zaslona in odmik prsta s površine zaslona), pri čemer si akciji sledita neposredno. Drsenje prsta po zaslonu vsebuje vse tri akcije, ki smo jih uporabili: DOWN, MOVE in UP. V našem primeru smo problem nekoliko poenostavili in zaporedju ne sledimo. Akcijo klik pa smo izvedli s pomočjo preverjanja koordinat akcije UP.
Delovanje kode lahko preverimo na napravi ali v simulatorju. Slika 1 prikazuje delujoč projekt z galerijo napadalcev zgoraj in ladjico spodaj. Če dovolj dolgo pritiskamo na vesoljsko ladjico, izginejo vsi napadalci, razen enega. Na sliki 2 je vidna tudi prednost dvojnega izrisa slik, saj sta napadalec in ladjica delno vidna tako na levem kot na desnem robu.
Nadgradnje projekta
Projekt lahko še malce dopolnimo z novimi tipi napadalcev poleg osnovnega zelenega. Dodali bomo še nekaj vrstic kode, in sicer: naložili bomo 3 nove slike, dodali bomo vertikalen odmik od zaslona in v galeriji slik napadalcev v nove vrste izrisali preostale tipe sovražnikov.
Izris več vrst napadalcev v obliki vrstic galerije
Uporabniki z manjšimi zasloni boste morda imeli težave s prekrivanjem zadnje vrste napadalcev in vesoljske ladjice. Enostavna rešitev problema je obrat aparata iz vodoravne v navpično lego. Na sliki 3 in 4 je viden projekt z dodanimi vrsticami slik sovražnikov. Na sliki 4 opazimo, da vesoljska ladjica s črnim robom prekriva svetlo modrega sovražnika v zadnji vrstici galerije napadalcev.
Problem velikosti zaslona glede na število elementov. Ladjica delno prekriva sovražnika.
Pri testiranju aplikacije na napravi lahko med obračanjem zaslona ugotovimo, da se poleg orientacije zaslona spremenijo tudi položaji napadalcev in vesoljske ladjice. Položaji oziroma odmik najbolj levega elementa v galeriji od levega roba se nastavi na osnovno vrednost. Pri zamenjavi orientacije zaslona se naš razred GameView spet požene z začetnimi vrednostmi spremenljivk. Kot smo omenili na začetku članka, bi tak problem lahko razrešili s shranjevanjem stanja med prehodi. Tako bomo našo igro dopolnili v enem izmed prihodnjih člankov.
Neodvisnost od ločljivosti
Problem predstavljene aplikacije je v tem, da je prilagojena zgolj za zaslone z ločljivostjo najmanj 1280 x 700 slikovnih pik. V primerih, ko želimo, da je naša aplikacija primerna tudi za naprave z drugačnimi zaslonskimi ločljivostmi, je treba pri izrisu uporabniškega vmesnika namesto enot slikovnih pik uporabljati relativne enote v deležih neke znane velikosti (npr. ločljivost zaslona).
Eden izmed načinov neodvisnosti od ločljivosti obsega enostavno preračunavanje velikosti izrisanih elementov glede na velikost zaslona. Metoda drawBitmap lahko kot argument sprejema tudi velikost pravokotnika, v katerega želimo izrisati sliko. Tako bi sliko izrisali s pomočjo pravokotnika (rect) velikosti, ki je odvisna od celotnega zaslona.
// doseci zelimo, da sirina napadalca zavzema
// eno desetino sirine zaslona
float s = this.width / 10 / invaderImage.getWidth(); // velikostni faktor
Rect pravokotnik = new Rectf(100*s, 100*s, 250*s, 250*s);
canvas.drawBitmap(invaderImage, null, pravokotnik, mPaint);
Slika se bo na zaslon izrisala z odmikom 100 slikovnih pik, pomnoženih z velikostnim faktorjem, od levega in zgornjega roba ter v velikosti 150 slikovnih pik, pomnoženih z velikostnim faktorjem, v vsako izmed dimenzij. Velikostni faktor je število, ki ga lahko izračunamo glede na znane lastnosti naprave (npr. širino zaslona) in znane velikosti uporabljenih elementov (npr. velikost slike vesoljske ladjice). V kodi je prikazan primer, kako doseči, da se sovražnik izrisuje v velikosti ene desetine širine zaslona. Projekt z aplikacijo, ki je neodvisna od ločljivosti, najdete na android.monitor.si.
Kako naprej?
V članku smo spoznali, kako lahko v aplikacijo vpeljemo različne načine interakcij in kako na različnih delih zaslona izvajamo med seboj neodvisne akcije. Spoznali smo tudi enega izmed primerov, ko je smiselno shranjevati stanje aplikacije, saj se v nasprotnem primeru aplikacija vnovič zažene v prvotnem stanju. Prikazali pa smo tudi, kako na zaslon izrisovati več slik hkrati in kako prilagoditi izris za naprave z različnimi zaslonskimi ločljivostmi. V prihodnjem delu se bomo podrobneje posvetili sami logiki in strukturi igre. Sestavili bomo objektni model igre, ki nam bo olajšal samo implementacijo. Vpeljali bomo tudi nove elemente, kot so izstrelki. Na koncu pa bomo vpeljali tudi glavno zanko igre, ki skrbi za izvajanje celotne igre. Projekt tega članka je tako kot prejšnji dostopen na android.monitor.si.