Android in razvoj 3D igre – Asteroidi, 2. del
Tokrat bomo v igro dodali komponente grafičnega vmesnika, ki nam bodo omogočile nadzor nad vesoljsko ladjo. Implementirali bomo možnost obračanja in pospeševanja vesoljske ladje. Prav tako bomo ladji dodali sferični ščit. Nazadnje bomo v igro dodali tudi zaznavanje trkov med vesoljsko ladjo in asteroidi. S tem bomo igro pripeljali korak bliže h končni podobi. Predstavljena vsebina je skupaj s programsko kodo dostopna na android.monitor.si.
Kot smo že po večjem številu uvodnih člankov ugotovili, je razvoj igre v treh razsežnostih dosti bolj zahteven kot razvoj igre v dveh razsežnostih. To smo si dokaj olajšali s tem, da pri razvoju uporabljamo razvojno ogrodje. V prejšnjem članku smo tako predstavili, kako narediti prve korake v razvoju igre, s tem da smo na prizorišče dodali 3D model vesoljske ladjice in asteroidov. Naj na tem mestu omenimo, da teh modelov nismo izdelali sami, temveč smo uporabili modele iz spleta, ki dovoljujejo nadaljnjo rabo. V našem primeru smo vesoljsko ladjico, ki je del zbirke modelov, priloženih spletni knjigi za učenje WebGLa (WebGL je vmesnik za uporabo OpenGLa v brskalnikih, morda se kdaj v prihodnosti seznanimo tudi s to tehnologijo), dostopni na spletnem repozitoriju GitHub (https://github.com/tparisi/WebGLBook). Asteroide pa smo si izposodili pri razvijalcu SolCommand, katerega domača stran je dostopna na naslovu: www.solcommand.com. Avtorjem se zahvaljujemo za njuno delo in za pripravljenost, da ga delita s skupnostjo razvijalcev.
V pričujočem članku bomo ogrodje igre, ki smo ga izdelali pretekli mesec, razširili z dodanim ozadjem, ki nam bo pričaralo občutek, da smo v vesolju. Prav tako bomo vesoljski ladji dodali ščit, ki se bo aktiviral ob trku z asteroidi in v primerih pobranega bonusa za aktivacijo ščita (bonuse bomo vključili v prihodnjih člankih). Dodali bomo osnovni grafični uporabniški vmesnik (angl. Graphical User Interface - GUI), s katerim bomo že lahko nadzirali ladjo in jo premikali po prostoru. Seveda bomo morali za to dodati tudi metode sukanja in premikov. Za konec bomo osvežili znanje detekcije trkov med predmeti v 3D prostoru in dodali odziv na tiste trke, ki se zgodijo med asteroidi in ladjo.
Slika 1: Prikaz vesoljske ladje med gibanjem
Premiki vesoljske ladje
Da nam bo bolj jasno, kako se premika ladja v vesolju, si najprej razložimo osnovno vedenje predmetov. Ko predmet v vesolju pospešimo v neko smer, se bo v to smer premikal s skoraj konstantno hitrostjo. Tu bomo stvari nekoliko poenostavili, saj bomo predpostavili, da se naša ladja giblje po praznem prostoru, kjer nanjo ne delujejo še kakšne dodatne gravitacijske sile nebesnih teles. Tako bomo premikanje ladjice skozi čas zgolj zelo počasi upočasnjevali s trenjem, kar lahko v kodi predstavimo na naslednji način:
public void update() {
...
// zmanjsevanje hitrosti premikanja
vx *= velocityFriction;
vy *= velocityFriction;
...
}
V zgornji kodi vx in vy predstavljata posamezni komponenti hitrosti premikanja ladjice oz. diferenciala, velocityFriction pa koeficienta trenja, ki je v našem primeru 0,99995f.
Pri sukanju (rotiranju) ladjice smo predpostavili, da lahko ladjico zasukamo tudi na mestu (kot bi uporabili stranske izpuhe). Pri tem jo bomo zaradi boljše vizualne predstave o rotaciji tudi nagnili v smer, kamor se vrti. Vrtenje ladjice bo bolj dušeno kot premikanje, to pomeni, da se bo ladjica pri tem hitreje umirila. Sukanje bomo izvajali s povečevanjem kotne hitrosti vrtenja ladjice. Tudi pri tem bomo ladjico umirili ob pomoči koeficienta trenja rotationalFriction, ki je v našem primeru nastavljen na 0,97f. Prav tako smo pri rotaciji določili maksimalni naklon, do katerega se ladjica pri tem še nagne. Rotacija je predstavljena s spodnjo programsko kodo:
public void update() {
...
// popravimo trenutno rotacijo s kotno
// hitrostjo
this.setRotZ( this.getRotZ() + vaz);
// upostevamo trenje pri rotaciji
vaz *= rotationalFriction;
if (Math.abs(vaz) < 0.1f) vaz = 0;
// popravimo nagib ladjice
if (Math.abs(shipGeom.getRotX() - 90) < 60 )
shipGeom.setRotX(shipGeom.getRotX() - vaz);
// nagib pocasi popravljamo proti osnovni
// legi
shipGeom.setRotX(shipGeom.getRotX()
- Math.signum(shipGeom.getRotX() - 90)
* 0.03f * Math.abs(shipGeom.getRotX() -
90));
...
}
Z zgornjimi vrsticami programske kode smo si pripravili temelje za premikanje in sukanje ladjice po vesolju. Marsikdo bo presenečen, da se ladjica ne vede po pričakovanjih. Naj še enkrat omenim, da lahko naša ladjica v določenem trenutku pospešuje zgolj v smer, v katero je obrnjena. To pomeni, da zaviramo tako, da jo najprej obrnemo, nato pa pospešujemo v nasprotno smer. Tako je narejena tudi izvirna igra Asteroids in prav tak nadzor nad ladjico da igri svojevrsten čar. Da bomo lahko našo ladjo nadzirali s pomočjo uporabniškega vmesnika, bomo definirali tudi ustrezne metode, ki bodo popravljale osnovne parametre gibanja in jih bomo klicali ob pritisku na izbrani gumb. Te metode so rotateLeft, rotateRight in accelerate. Kasneje bomo dodali še več podobnih metod za nadzor ladje in proženje akcij (npr. streljanje). V nadaljevanju je prikazana programska koda omenjenih metod. Metodi za obračanje ladje pa sta res zelo preprosti:
public void rotateLeft() {
vaz += 0.25f;
}
public void rotateRight() {
vaz -= 0.25f;
}
Za metodo pospeševanja je treba uporabiti tudi nekaj znanja osnov trigonometrije, saj moramo ladjo pospešiti v pravilni smeri. To storimo z množenjem posamezne komponente hitrosti z ustreznim količnikom. Ladjico ob vsakem pritisku pospešimo v smeri, kamor je obrnjena, za delež 0,0005f. Šele nato ustrezno povečamo posamezno komponento hitrosti:
public void accelerate() {
// uporabimo trigonometricni funkciji
// sinus in cosinus za izracun prispevka
// posamezni komponenti hitrosti
float dx = (float)Math.cos((float)Math.
toRadians(this.getRotZ())) * 0.0005f;
float dy = (float)Math.sin((float)Math.
toRadians(this.getRotZ())) * 0.0005f;
vx += dx;
vy += dy;
}
Grafični uporabniški vmesnik
Danes je večina androidnih telefonov brez tipkovnice in se jih upravlja z dotiki. Tako je treba za interakcijo poskrbeti drugače kot včasih. Zato bomo v našem primeru na zaslon aplikacije dodali nekaj temeljnih prvin vmesnika, ki nam bodo omogočale osnovni nadzor nad našo vesoljsko ladjo. Dodali bomo štiri gumbe z naslednjimi akcijami: zasuk levo, zasuk desno, pospeševanje in streljanje. Gumbe bomo postavili na ustrezna mesta na zaslonu, kjer bodo med igranjem najlaže dostopni in ne bodo ovirali pregleda nad dogajanjem v igri. V tem članku bomo izvedli akcije za izvedbo rotacije v obe smeri in pospeševanje, akcijo streljanja pa bomo prihranili za naslednji mesec.
Izgradnje vmesnika se bomo lotili povsem programsko. Razvojno okolje Eclipse nam sicer dopušča tudi grafično načrtovanje uporabniškega vmesnika, a te možnosti ne bomo uporabili in bomo prikazali izgradnjo vmesnika v programski kodi. V vmesniku bomo pripravili tudi dva napisa, ki bosta prikazovala število življenj igralca in trenutno dosežene točke v igri. Implementacijo nagrajevanja igralca s točkami bomo razvili kasneje skupaj z lestvico najboljših.
Za ustrezno razporeditev elementov uporabniškega vmesnika po celotni površini zaslona je treba uporabiti ustrezne vsebovalnike in elemente znotraj slednjih primerno razporediti. Pri implementaciji elementov uporabniškega vmesnika se hitro nabere večje in manj pregledno število vrstic programske kode, s katero določamo lastnosti grafičnih elementov in njihovo razporeditev. Da bi se izognili nepreglednosti, bomo definirali nov razred HUD prav za definiranje uporabniškega vmesnika, ki bo vseboval metode za prikaz različnih delov vmesnika ob različnih stanjih naše igre. V tem članku bomo implementirali zgolj del vmesnika, ki je prikazan med samo igro. Ob stvaritvi novega vmesnika bomo podali referenco na osnovni razred naše igre (this), referenco na osnovni vsebovalnik mLayout, v katerega bomo vstavili celoten vmesnik, referenco na izrisovalnik mRenderer ter širino in višino okna aplikacije. Po kreiranju primerka razreda HUD kličemo še metodo, ki poskrbi za izris vmesnika med igranjem.
HUD hud = new HUD(this, mLayout,
mRenderer, width, height);
hud.gameMenuHUD();
Osnovni vsebovalnik, ki nam je na voljo v naši aktivnosti (mLayout), je tipa FrameLayout. Vanj bomo vključili celoten grafični vmesnik naše igre, v kasnejših člankih pa tudi vmesnik osnovnega menuja igre in druge vmesnike (pomoč in lestvico najboljših rezultatov). Celoten med igro prikazan vmesnik bomo sestavili v vsebovalnik gameHUDLayout tipa LinearLayout. Zgradba vmesnika je predstavljena na sliki 2.
Slika 2: Zgradba celotnega uporabniškega vmesnika, ki je prikazan med samo igro (a), zgled označenega in neoznačenega gumba (b)
Iz slike je razvidno, da je vmesnik sestavljen iz večjega števila komponent, kar predstavlja kar precejšnje število vrstic programske kode, ki je na tem mestu nimamo prostora prikazati. Vsako izmed komponent je treba inicializirati in vstaviti v primerno starševsko komponento. To se izkaže v želeni razporeditvi elementov zaslona. Oglejmo si še zgled dodajanja poslušalca, ki se odziva na dotike na komponenti. Ob dotiku bomo tako sprožili akcijo v igri kot vizualno sporočilo uporabniku, da je bil gumb pritisnjen z zamenjavo prikazane sličice gumba, tudi to je razvidno s slike 2. Gumb, ki se ga igralec dotakne, je obarvan svetleje. Spodaj je podana še programska koda, ki sproži akcijo:
rotateLeft.setOnTouchListener(new
OnTouchListener() {
public boolean onTouch(View v,
MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// ob dotiku - svetla slicica
rotateLeft.setImageResource(R.
drawable.gui_left_down);
break;
case MotionEvent.ACTION_UP:
// ob koncu dotika - temnejsa slicica
rotateLeft.setImageResource(R.
drawable.gui_left);
break;
}
// klic metode, ki zasuce ladjo
mRenderer.rotateLeft();
return true;
}
});
Zaznava trkov med asteroidi in ladjo
Preverjanje trkov med posameznimi elementi v 3D okolju lahko predstavlja zelo zahteven problem, saj terja dobro izvedbo, zelo dobro poznavanje matematike, a se v to tu ne bomo spuščali. V našem primeru bomo uporabili kar v ogrodje Rajawali vgrajeno podporo detekciji trkov. Da pa bo vse skupaj še bolj enostavno, ne bomo implementirali nobenih odbojev, temveč bomo po trku preprosto uničili asteroid in ladjici aktivirali ščit s klicem metode, definirane kasneje v članku.
Slika 3: Prikaz sfer okoli asteroidov in ladje, za katere se dejansko izvaja preverjanje trkov.
Ogrodje Rajawali nam ponuja uporabo metode vsebujočih škatel ali vsebujočih sfer, mi bomo uporabili slednje. Tako bomo ladjo in asteroide obdali z vsebujočimi sferami, za katere nam bo ogrodje znalo povedati, ali so v določenem trenutku udeležene v trku ali ne. Slika 3 prikazuje vsebujoče sfere asteroidov in ladje. Vse skupaj bomo izvedli v metodi checkCollisions v razredu AsteroidsRenderer. Da pa bomo lahko preverjali trke, bomo potrebovali še nekaj dopolnitev razredov Asteroid in Ship. V konstruktorju razreda Asteroid bomo dodali spodnje vrstice, ki asteroid obdajo z nevidno sfero:
boundingSphereGeom = new Sphere(0.15f, 16, 16);
boundingSphereGeom.setMaterial(new
SimpleAlphaMaterial());
boundingSphereGeom.setTransparent(true);
addChild(boundingSphereGeom);
V metodo update pa spodnjo kodo za osveževanje transformacije pomožne sfere:
boundingSphere = boundingSphereGeom.
getGeometry().getBoundingSphere();
boundingSphere.transform(boundingSphereGeom.
getModelMatrix());
Dodali bomo še metodo, s katero bomo vračali vsebujočo sfero:
public IBoundingVolume getBoundingSphere() {
return boundingSphere;
}
Prav tako bomo dodali še pomožno metodo za uničenje asteroida destroyAsteroid, ki bo ob klicu asteroid odstranila s prizorišča. Vsebine metode ne bomo posebej navajali, saj je precej preprosta. Podobno bomo dopolnili še razred Ship. V konstruktor bomo dodali:
boundingSphere = boundingSphereGeom.
getGeometry().getBoundingSphere();
boundingSphere.transform(boundingSphereGeom.
getModelMatrix());
Dodali bomo enako metodo kot v razredu Asteroid, ki vrača vsebujočo sfero. Nazadnje naj predstavimo še metodo checkCollisions, ki dejansko preverja trke:
private void checkCollisions() {
for (Asteroid a : asteroids) {
// ali ladja in asteroid trkata
if (a.getBoundingSphere().intersectsWith(ship.getBoundingSphere()) &&
a.isAlive()) {
ship.activateShield();
if (this.hasChild(a)) {
a.destroyAsteroid();
this.removeChild(a);
}
}
}
}
Metodo bomo v kasnejših člankih dopolnili še s preverjanjem drugih trkov.
Dodajanje vesoljskega ozadja
Da bo igra bolj slikovita in barvita, bomo dodali sliko za ozadje, ki nas bo prestavila v lepote vesolja. V našem primeru bomo tako v ozadje dodali sliko vesoljske meglice. Da pa ozadje ne bo povsem statično in monotono, bomo pred sliko meglice dodali še polprosojno teksturo megle, ki bo določene dele meglice dodatno zakrila. Prav tako bomo dodali nekaj dinamičnosti, in sicer tako, da bomo sliko vesoljske meglice zelo počasi sukali okoli središča zaslona v eno smer, v nasprotno smer pa bomo vrteli polprosojno teksuro megle. S tem bomo pričarali učinek spremenljivega dinamičnega vesolja.
Slika 4: Slika vesoljske meglice (zgoraj) in tekstura polprosojne megle na beli podlagi (spodaj)
Za potrebe izrisa ozadja bomo definirali nov razred StarField. Razred bomo lahko uporabili tako za prikaz slike ozadja kot tudi za prikaz meglice. To bomo določili s parametrom konstruktorja. Med vire bomo tako dodali sliko ozadja (npr. stars.png), ki je prikazana na sliki 4 zgoraj, in polprosojno teksturo megle (npr. fog.png) s slike 4 spodaj.
Tako kot drugi predmeti v prostoru bo tudi nov razred razširjal razred BaseObject3D. Spodaj je podana definicija razreda:
public class StarField extends BaseObject3D {
// definicija lokalnih spremenljivk
...
// staticna definicija konstant
static {
fields[0] = R.drawable.stars;
fields[1] = R.drawable.fog;
}
public StarField(Resources r,
TextureManager tm, ALight dl, boolean f) {
fog = f;
fieldVariant = fog ? 1 : 0;
// priprava teksture
BitmapFactory.Options opts = new
BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.
Config.ARGB_8888;
fieldTexture = BitmapFactory.
decodeResource(r,
fields[fieldVariant], opts);
background = new Plane(7.2f/1.5f,
12.8f/1.5f, 1, 1);
fieldMaterial = new SimpleMaterial();
// nastavitev lastnosti ozadja
background.setMaterial(fieldMaterial);
background.addTexture(tm.
addTexture(fieldTexture));
background.setTransparent(true);
background.setRotZ(-90);
background.setScale(1.75f);
background.setZ( fog ? 0.5f : 1.0f);
this.addChild(background);
}
public void update() {
background.setRotZ(background.
getRotZ() + (fog ? 0.1f : -0.01f ));
}
}
Celotno ozadje je tako predstavljeno z ravnino, na katero prilepimo sliko. Ob zvezdnem ozadju je to slika zvezd in umaknemo jo dlje v ozadje (1,0f po Z komponenti), v primeru meglice pa jo prestavimo med druge predmete in zvezdno nebo (0,5f po Z komponenti). V metodi update poskrbimo tudi za ustrezno sukanje posamezne plasti ozadja. Da uporabnik ne opazi, kako je dejansko sestavljeno ozadje, moramo paziti, da so ploskve, na katere smo prilepili slike, dovolj velike, da med vrtenjem v vidno polje ne zaide kateri izmed robov.
Ščit
Da bi se v igri na zanimiv način izognili prezgodnji smrti takoj ob začetku s trkom z asteroidom in da bi igralcu tudi kasneje v igri omogočili odpustek v obliki bonusa, bomo v igro vpeljali možnost ščita. Ščit predstavlja sposobnost prehoda ladje skozi asteroid namesto trka, do katerega bi sicer prišlo. Ob zagonu igre, ko igralec še prevzema nadzor nad ladjo, proti njemu pa že grozeče hiti asteroid, je to skoraj nujna pomoč, saj bi igralec v nasprotnem primeru zaradi občutka nemoči kaj hitro obupal nad igro. Ker pa želimo igralcu dodati še dodaten izziv v obliki bonusov, bomo prav ščit ponudili kot enega izmed bonusov, ki ga bo uporabnik lahko pobral.
Slika 5: Tekstura, ki jo prilepimo na sfero ščita (levo) in aktiviran ščit v igri (desno).
Ščit bomo predstavili s sfero, ki bo vsebovala celotno ladjo. Sfera se bo po prostoru pomikala skupaj z ladjo, za boljši učinek pa bo imela prosojno teksturo, ki je prikazana v sliki 5 in in se bo vrtela okoli ene izmed svojih osi. S tem bomo dobili občutek zanimivega električnega ščita. Ščit bomo implementirali kot del vesoljske ladje. V razredu Ship bomo dodali krajevne spremenljivke:
// sfera, ki predstavlja scit
private Sphere shieldSphere;
// prosojen material za scit
private SimpleAlphaMaterial shieldMaterial;
// stikalo za prikaz scite
private boolean shieldUp = false;
// casovno trajanja scita
private final int shieldTimerLimit = 10000;
// casovnik za trajanje scita
private long shieldTimer = 0;
Prav tako moramo razširiti tudi konstruktor, kjer ustvarimo ustrezne predmete, nastavimo materiale, inicializiramo časovnik. Razširitev je predstavljena spodaj:
// nastavitve za prosojno teksturo
BitmapFactory.Options opts = new
BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.
ARGB_8888;
shieldTexture = BitmapFactory.
decodeResource(r, R.drawable.shield, opts);
// kreiramo novo sfero in material
shieldSphere = new Sphere(0.2f, 16, 16);
shieldMaterial = new SimpleAlphaMaterial();
// nastavitve sfere
shieldSphere.setDoubleSided(true);
shieldSphere.setMaterial(shieldMaterial);
shieldSphere.addTexture(tm.
addTexture(shieldTexture));
shieldSphere.setTransparent(true);
// sfero dodamo v sceno
addChild(shieldSphere);
// določimo začetni cas scita
shieldTimer = System.currentTimeMillis();
Ščit moramo med izrisom tudi osveževati, saj po preteku predvidenega časa ščit izgine. Tako si bomo definirali pomožni metodi, ki aktivirata in deaktivirata ščit, in metodo, ki sporoča, ali je ščit aktiven ali ne.
public void deactivateShield() {
removeChild(shieldSphere);
this.shieldUp = false;
}
public void activateShield() {
if (!shieldUp) {
addChild(shieldSphere);
shieldTimer = System.
currentTimeMillis();
this.shieldUp = true;
}
}
public boolean isShieldActivated() {
return this.shieldUp;
}
Ne nazadnje moramo poskrbeti še za osveževanje ščita in preverjanje, ali je čas trajanja že potekel ali ne. To bomo storili v metodi update, kot je prikazano spodaj:
...
if (System.currentTimeMillis() -
shieldTimer > shieldTimerLimit)
deactivateShield();
if (shieldUp)
shieldSphere.setRotY(shieldSphere.
getRotY() + 0.5f);
...
S tem smo končali implementacijo ščita. Ščit lahko tako aktiviramo ali deaktiviramo s preprostim klicem ustrezne metode. To lahko izkoristimo ob zaznanem trku.
• • •
V članku smo predstavili marsikaj novega, kar se pozna tudi na dodelani igri. Igri smo nadgradili videz, prav tako smo dodali tudi funkcionalnost zaznavanja trkov med asteroidi in ladjo, kar že lahko izkoristimo in preverimo, ali je igra zasnovana dovolj dobro, da se bo igralec lahko trkom izmikal ali ne. Poleg grafičnih »obližev« pa smo dodali tudi uporabniški vmesnik, ki nam končno omogoča tudi samo interakcijo z igro. Igro lahko tako že preizkusijo tudi prijatelji in nam podajo prve odzive. V prihajajočih člankih bomo igro še izpopolnili in pripeljali korak bliže h končnemu izdelku.