Objavljeno: 23.4.2013 | Avtor: Matevž Pesek, Ciril Bohak | Monitor Maj 2013 | Teme: 3d, android, programiranje

Android in razvoj 3D igre – Asteroidi

Tokrat bomo začeli z višjenivojskim razvojem 3D igre Asteroidi. V preteklih člankih smo si ogledali temeljne koncepte in delovanje 3D grafike ter predstavili programsko ogrodje Rajawali, s katerim si bomo olajšali razvoj igre. Ustvarili bomo 3D svet in vanj naložili in umestili vesoljsko ladjo ter asteroide. Implementirali bomo tudi prve premike predmetov v navideznem vesolju in se poglobili v načine naključnega izrisa predmetov brez prekrivanja. Aplikacija bo na koncu že dobila obliko, podobno izvirni igri Asteroids. Vsebina članka je skupaj s programsko kodo dosegljiva na android.monitor.si.

V preteklem letu smo predstavili razvoj slavne retroigre Space Invaders. Igro smo razvili v 2D načinu, da bi posnemali izvirno igro. Tudi tokrat bomo ostali v vesolju in se lotili razvoja igre Asteroidov (Asteroids), a jo bomo razširili v 3D način in s tem izkoristili zmogljivosti androidne platforme. Asteroidi so legendarna igra, ki se je v svoji prvi inkarnaciji znašla na igralnih avtomatih že leta 1979. Tedaj igralni avtomati še niso imeli mer, primernih za domačo rabo, zato so bili v javnih prostorih, kjer so se mimoidoči zabavali, družili in podirali rekorde v rezultatih iger lokalne igričarske skupnosti. Igra je zaradi tekmovalne narave zbiranja točk in posledičnega uspeha prinašala veselje tudi lastnikom avtomatov, saj je bil vsak poizkus igre plačljiv. Kasneje se je igra znašla v več različicah na domačih konzolah in drugih prenosnih igralnih aparatih in računalniku. Cilj igre je podoben tistemu pri igri Space Invaders – preživeti čim dlje časa in pri tem uničiti čim več sovražnikov, saj slednji prinašajo točke.

Implementacija okolja

Igro bomo razvili v 3D okolju ob pomoči predstavljenega programskega ogrodja Rajawali. Kot je bilo že podrobneje razloženo, v programskem okolju Eclipse sprva uvozimo projekt Rajawali in ga v obliki datoteke ZIP prenesemo s spletne strani Github. Nato ustvarimo svoj androidni projekt s poljubnim imenom. Pod lastnostmi projekta v zavihku Android in razdelku Library dodamo pravkar uvoženi projekt Rajawali kot referenco. Tako smo dobili dostop do razredov ogrodja, ki jih bomo uporabili pri implementaciji igre.

Prav tako se bomo lotili nove aktivnosti, ki razširja razred RajawaliActivity, in lastnega razreda AsteroidsRenderer, ki razširja razred RajawaliRenderer. Natančnejši postopek implementacije je opisan v prejšnjem članku, programska koda pa je dostopna na spletni strani android.monitor.si. Naprednejšim je na voljo tudi spletna stran z dokumentacijo ogrodja, saj bomo tokrat uporabili le majhen del funkcionalnosti, ki jih ponuja ogrodje.

Vnos predmetov in nastavitve 3D sveta

Tokrat ne bomo uporabljali enostavnih predmetov, temveč bomo kompleksnejše že pripravljene  uvozili v projekt. V mapi src sprva ustvarimo novo mapo z imenom raw. Okolje eclipse mapo raw prepozna kot zbirališče datotek, ki so v »surovem« formatu – vrsta in vsebina datotek se ne preverja, pravilna in smiselna raba in razbiranje informacij v vsebini datotek je prepuščeno programerju. V tej mapi bomo shranili vse predmete asteroidov in vesoljske ladje. Model vesoljske ladje smo poimenovali ship_obj, datoteke asteroidov pa z zaporedno oznako med ast1a_obj in ast6e_obj. Naj  omenimo še, da morajo biti datoteke v tej mapi ustrezno poimenovane. Ker se imena datotek preslikajo v imena spremenljivk v okolju, morajo biti imena zgolj iz malih črk, številk in podčrtajev. Praviloma opustimo tudi končnice. Datoteke so zdaj del projekta (lahko jih naslavljamo prek razreda R), ostane le še implementacija nalaganja predmeta v samo aplikacijo in raba. V ta namen bomo zaradi boljše preglednosti projektu dodali nov razred Ship.java in v njem z dodatnimi spremenljivkami nadzorovali parametre. Definirajmo konstruktor in izluščimo predmet skupaj s teksturami. Spremenljivke, ki jih naslavljamo v kodi, so definirane znotraj razreda.

public Ship(Resources r, TextureManager tm) {

        parser = new ObjParser(r, tm, R.raw.

            ship_obj);

        parser.parse();

        shipGeom = parser.getParsedObject();

}

V spremenljivko shipGeom smo ob pomoči razčlenjevalnika ObjParser naložili predmet, ki ga bomo prikazovali. Ker je razmeroma velik glede na naše predvideno vidno polje, ga pomanjšajmo:

shipGeom.setScale(0.01f);

Pozornost namenimo še pogosti napaki pri uvozu predmeta. Z uporabo funkcije samodopolnjevanja v programskem okolju Eclipse lahko nevede namesto datoteke virov naš_paket.R vključimo datoteko android.R, kar se bo izkazalo v napaki, saj reference ship_obj v datoteki R na videz ne bo. Napako lahko odpravimo z ročnim popravkom vrstice include, kjer paket android popravimo v ime našega paketa.

Slika 1: Tako je bila videti prva inkarnacija igre Asteroids (levo) na Atarijevi igralni konzoli (desno).

Slika 1: Tako je bila videti prva inkarnacija igre Asteroids (levo) na Atarijevi igralni konzoli (desno).

V razredu Renderer moramo poskrbeti za klic konstruktorja ladje. V metodi onSurfaceCreated bomo uvedli preverjanje, ali je scena že naložena. Ob prvem klicu bomo izvedli metodo initScene(), ki bo poskrbela za nalaganje predmetov v prostor. Oglejmo si pripravo prostora in nalaganje ladje v prostor. Spremenljivki mLight in mCamera sta definirani znotraj ogrodja.

//nastavitve luci

mLight = new DirectionalLight(1.0f, 2.0f,

    1.0f);

mLight.setColor(1.0f, 1.0f, 1.0f);

mLight.setPower(3);

//pomakni kamero

mCamera.setZ(-4.0f);

//inicializacija ladje

ship = new Ship(mContext.getResources(),

    mTextureManager);

ship.addLight(mLight);

addChild(ship);

V metodi smo ustvarili novo usmerjeno luč. Luči smo določili barvo po komponentah in določili moč svetila. Številke so izbrane empirično, z malo igranja s spremembami pa lahko dobimo povsem drugo predstavo sveta. Kamero smo umaknili za štiri enote nazaj glede na središče koordinatnega sistema. Premik kamere smo izvedli z namenom izrisovanja vesoljske ladje v središču koordinatnega sistema –  točki (0,0,0). Kasneje bomo položaje predmetov ob premiku med posodabljanjem stanja izrisanega sveta izračunavali glede na vidno polje. Postavitev je relativna, velja pa določiti okvirne meje izrisa, saj nam to olajša kasnejšo izvedbo premikov. V našem primeru smo se odločili za premik kamere iz središča koordinatnega sistema v smeri osi Z, ki kaže neposredno v zaslon, saj gre pri ogrodju Rajawali za desnosučni sistem. Večina razvojnih orodij sicer uporablja levosučni sistem. V tem primeru bi bila os Z sistema obrnjena pravokotno iz zaslona proti uporabniku.

S klicem konstruktorja ladje podamo referenco na vire, v katerih je datoteka s 3D predmetom in urejevalnik tekstur (angl. Texture Manager), ki ga razčlenjevalnik predmetov uporabi za nalaganje teksture ob nalaganju predmeta.

Prvi rezultat implementirane kode lahko preizkusimo na svoji mobilni napravi. Za boljšo predstavo smo vesoljsko ladjo še malce zavrteli – v razredu Ship smo se poigrali z metodama rotacije po oseh X in Y na naloženem predmetu shipGeom.setRotX in shipGeom.setRotY.

Vesoljsko ladjo bomo pomanjšali še za en velikostni razred in v prostor na naključna mesta postavili asteroide. Sprva dodajmo nov razred Asteroid. Da bi bila igra grafično čim bolj zanimiva, smo pripravili več grafičnih modelov asteroidov. Model za posamezno instanco razreda Asteroid bomo naložili naključno izmed 36 modelov, ki so priloženi projektu.

V razredu Asteroid smo poprej nastavili polje vseh možnosti 3D predmeta asteroida. Skrajšan opis kode je videti tako:

static {

  asteroidVariants[0] = R.raw.ast1a_obj;

  asteroidVariants[1] = R.raw.ast1b_obj; ...

  ... asteroidVariants[35] = R.raw.ast6e_obj;

  asteroidVariants[35] = R.raw.ast6f_obj;

}

Model bomo naključno izbrali v konstruktorju in mu določili faktor velikosti.

public Asteroid(Resources r, TextureManager

tm) {

    asteroidVariant = (int)Math.floor( Math.

       random() * asteroidVariants.length );

    parser = new ObjParser(r, tm,

        asteroidVariants[asteroidVariant]);

    parser.parse();

    asteroidGeom = parser.

        getParsedObject();

    asteroidGeom.setScale(0.05f);

    this.addChild(asteroidGeom);

...

Slika 2: Prvi rezultat naloženega predmeta. Vesoljska ladja je trenutno statična v 3D prostoru.

Slika 2: Prvi rezultat naloženega predmeta. Vesoljska ladja je trenutno statična v 3D prostoru.

Ker želimo, da se asteroidi naključno premikajo po prostoru, bomo določili spremenljivki tipa float, ki bosta hranili komponente vektorja premika – hitrosti v prostoru. Vektor premika bo razmeroma majhen, saj želimo doseči počasno premikanje po prostoru in dati igralcu čas, da vesoljsko ladjo pravočasno premakne ali uniči asteroide brez trka. Asteroidi se bodo navidezno premikali v ravnini, zato potrebujemo le dve komponenti, saj bomo vse predmete izrisovali v isti navidezni ravnini.

//naključno izbrani komponenti vektorja smeri

this.vx = (float)Math.random() * 0.005f -

    0.0025f;

this.vy = (float)Math.random() * 0.005f -

    0.0025f;

Spremenljivki vx in vy ne ponazarjata absolutnih vrednosti mesta predmeta, temveč diferencial (delček) poti, v katero želimo predmet premakniti. Da bi popestrili premike asteroidov in izboljšali uporabniško izkušnjo dogajanja v sami igri, bomo posamezen asteroid naključno zasukali v vseh treh dimenzijah. Rotacija za posamezno dimenzijo je shranjena v spremenljivkah vax, vay in vaz.

//naključno izbrana rotacija asteroida v prostoru

this.vax = (float)Math.random() * 5f - 2.5f;

this.vay = (float)Math.random() * 5f - 2.5f;

this.vaz = (float)Math.random() * 5f - 2.5f;

Naključna postavitev asteroidov

Za dejanski prikaz predmetov moramo izdelati primerke razreda Asteroid in predmet postaviti na naključno mesto v prostoru. Sprva naključna postavitev ne povzroča težav, a moramo paziti, da se predmeti med seboj ne prekrivajo, saj lahko to prinese dodatne težave kasneje, ko bomo implementirali detekcijo trkov. Tako na primer ne smemo izrisati asteroida na mestu vesoljske ladje, saj bi takšna postavitev pomenila takojšnje uničenje ladje in konec igre. Položaje moramo izbirati naključno in pri tem paziti, da na izbranem polju ni drugega predmeta. Zato naključni položaj izbiramo tako dolgo, dokler ne pridobimo prostega mesta. Izbrani položaj nastavimo asteroidu in ga pripnemo razredu AsteroidsRenderer. Zaradi enostavnosti smo tokrat izbrali preprost izračun prostega mesta, ki nastavi isti začetni položaj asteroida po vsaki komponenti z dodanega malce naključja.

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

    asteroids.add(new   Asteroid(mContext.getResources(), mTextureManager));

    asteroids.get(i).addLight(mLight);

    int rndi = 0;

    while (true) {

        rndi = rnd.nextInt(asteroidPositions.

            size());

        if ( ! takenAsteroidsPositions.

            contains(rndi) ) {

            takenAsteroidsPositions.add(rndi);

            break;

        }

    }

    asteroids.get(i).setX(asteroidPositions.

        get(rndi)[0] + (float)Math.random()

        * 0.1f - 0.05f);

    asteroids.get(i).setY(asteroidPositions.

        get(rndi)[1] + (float)Math.random()

        * 0.1f - 0.05f);

    addChild(asteroids.get(i));

}

Ker pri programiranju pogosto naletimo na take težave, si oglejmo našo implementacijo izbire naključnega mesta za asteroid. Vidno polje je v naši igri široko približno 5 (med –2,5 in 2,5) enot in visoko 3 (med –1,5 in 1,5) enote. Naključno generiranje števil na teh dveh intervalih je prva izmed možnosti, ki se nam porodi pri implementaciji algoritma. Pri tem pa moramo upoštevati velikost predmetov, ki so že postavljeni v prostoru, zato moramo za vsako izbrano točko preverjati še mejne vrednosti, ki jih bo predvidoma zasedel novovstavljeni predmet. Če na primer izberemo točko x = 1,5 in y = 1,5 in vemo, da asteroid zaseda prostor, primerljiv s sfero z radijem 0,2, moramo pred vstavljanjem preveriti, ali na območju za x = [1,3, 1,7] in y = [1,3, 1,7] ni nobenega predmeta niti njegovih skrajno zunanjih delov. Če upoštevamo velikost že vstavljenih predmetov, moramo preverjati položaj predmetov na intervalih x = [1,1, 1,9] in y = [1,1, 1,9] za vsak že vstavljeni predmet. Takšno vstavljanje se zelo hitro podaljša v opazno počasno izbiro. V podanem primeru smo položaj izračunali preblizu roba (y koordinata preseže vrednost 1,5), kar moramo tudi upoštevati. Z vsakim novim predmetom, ki ga želimo vstaviti, se poveča možnost, da bodo naključno izbrana števila sovpadala z že izkoriščenimi intervali vstavljenih predmetov. Če želimo vstaviti 20 asteroidov, se bomo pošteno načakali. Paziti moramo tudi na to, da ne vstavimo preveč predmetov, saj je možno, da vseh predmetov v vidno polje ni mogoče vstaviti (bodisi zaradi razporeditve, ob velikem številu pa tudi zaradi pomanjkanja prostora).

Druga možnost, ki smo jo uporabili v zgoraj opisani kodi, prav tako vsebuje »požrešno« iskanje nezasedenega prostora, a izbira naključna mesta bolj učinkovito in zato potrebuje manj časa. Požrešnost (angl. »greedy«) pri algoritmih označujemo takrat, ko v postopku iskanja najboljše rešitve vzamemo prvo ali trenutno najboljšo, ne da bi pri tem upoštevali, kako to vpliva na optimalnost končnega rezultata.  V našem primeru je rezultat naključna porazdelitev, zato se na optimalnost (na primer enakomernost porazdelitve) ne oziramo. Polje smo tokrat razdelili na mrežo. Vsa vozlišča v tej mreži bomo navidezno oštevilčili med 0 in n, a te meje ne bomo presegli – število asteroidov ne bo preseglo števila vozlišč, vozlišča pa bodo med seboj razmaknjena za 0,5 enote. Tako ustvarjena navidezna vozlišča bomo poprej, v statičnem delu inicializacije razreda, shranili v zbirko asteroidPositions.

for (float i = -2.5f; i <= 2.5f; i+=0.5f)

    for  (float j = -1.5f; j <= 1.5f; j+=0.5f)

    if ( Math.round(i*10) != 5 && Math.

        round(j) != 0)

        asteroidPositions.add(new float[]

            {i, j});

Z vsakim vstavljanjem asteroida v prostor bomo preprosto izbrali naključno lokacijo v zbirki, ki vsebuje vrednosti vsake komponente iz zbirke asteroidPositions. Tokrat ni treba preverjati meja že vstavljenih predmetov, saj vemo, da so preostali predmeti postavljeni na mreži in ne more priti do sovpadanja oziroma medsebojnega prekrivanja. Prav tako ni treba preverjati položaja vesoljske ladje, saj smo ta položaj izločili že pri izbiri vozlišč na mreži. Edina možnost sovpadanja je pri poskusu vstavljanja predmeta na isto mesto, ki je že zasedeno na mreži. To preverjamo ob vsaki izbiri naključnega števila. Ker se lahko vzorec vstavljanja vidi pri izrisu (pri daljšem vstavljanju dobimo šahovski vzorec izrisanih asteroidov in ozadja), vrednosti izbranega mesta še naključno popravimo za majhno vrednost.

Premiki asteroidov

Ker želimo izrisovati rotacije in premike, moramo v razredu Asteroid dodati metodo update, ki bo osveževala stanje predmetov. V razredu Ship bomo prav tako dodali isto metodo in simulirali rotacijo ladjice, saj še nismo dodali krmilnih gumbov za samo usmerjanje.

Trenutno stanje omogoča izris asteroidov znotraj vidnega polja. Vsakemu asteroidu v metodi update določimo novo vrednost glede na stari položaj predmeta in ji prištejemo diferencial premika po vsaki komponenti, shranjenem v spremenljivkah vx in vy.

public void update() {

  this.setX( this.getX() + vx);

  this.setY( this.getY() + vy);

}

S tem dosežemo želene premike asteroidov, a se ob zagonu aplikacije asteroidi premaknejo ven iz vidnega polja in odlebdijo daleč stran. Ena izmed rešitev je omejitev robov. Če določimo strogo mejo, ki je asteroidi ne smejo preseči, moramo predmetu spremeniti smer glede na odboj od navidezne ovire. Taka rešitev sicer dosega ohranitev asteroidov znotraj vidnega polja, ob igranju pa dobimo občutek zaprtega prostora. Druga možnost je navidezen prehod z ene strani zaslona na drugo. Takšna rešitev navidezno poveže robove zaslona in daje občutek premikanja po sferičnem svetu brez robov.  Podrobneje si oglejmo metodo osveževanja stanja asteroida.

public void update() {

    float xLimit = 3.0f;

    float yLimit = 2.0f;

    this.setX( this.getX() > xLimit ?

        -xLimit : ( this.getX() < -xLimit ?

        xLimit : this.getX() + vx ) );

    this.setY( this.getY() > yLimit ?

        -yLimit : ( this.getY() < -yLimit ?

        yLimit : this.getY() + vy ) );

Na začetku definirajmo spremenljivki xLimit in yLimit, ki določata strogi meji našega vidnega prostora. Z mejo želimo doseči navidezno preslikavo predmeta ob prehodu čez enega izmed robov v prikaz predmeta na nasprotnem robu. Navidezno se bodo robovi našega sveta zlepili skupaj v sfero. Ob premiku asteroida na sredini zaslona le prištejemo vsaki komponenti this.setX in this.setY trenutne vrednosti (getX in getY) skupaj z vektorjem smeri, shranjenim v spremenljivkah vx in vy.

Obrat asteroida v vsako izmed treh razsežnosti izvedemo podobno, z metodami setRot* in spremenljivkami vax, vay in vaz. Ker predmet ob vsakem osveževanju izrisujemo ob pomoči treh operacij – rotacije (setRot*), translacije (set*) in skalacije (setScale) – moramo tudi ob simulaciji vrtenja asteroida rotiranju neprestano dodajati diferencial, ki smo ga poprej naključno določili.

asteroidGeom.setRotX(asteroidGeom.getRotX()

    + this.vax);

asteroidGeom.setRotY(asteroidGeom.getRotY()

    + this.vay);

asteroidGeom.setRotZ(asteroidGeom.getRotZ()

    + this.vaz);

Za izvedbo metode update moramo poskrbeti s klicem slednje znotraj metode onDrawFrame razreda AsteroidsRenderer.

Dosedanji napredek aplikacije lahko preverimo z zagonom igre. Slika 3 prikazuje zgled zagona z naključno razporejenimi asteroidi in vesoljsko ladjo, ki je postavljena na sredino vidnega polja v desetkrat manjšem merilu od prejšnjega prikaza na sliki 2.

Slika 3: Prikaz vesoljske ladje z naključno razporejenimi asteroidi. Ladjo smo dodatno pomanjšali in s tem dosegli prikaz, podoben prvotni igri.

Slika 3: Prikaz vesoljske ladje z naključno razporejenimi asteroidi. Ladjo smo dodatno pomanjšali in s tem dosegli prikaz, podoben prvotni igri.

• • •

Tokrat smo implementirali nalaganje in izris vesoljske ladje in asteroidov. S slednjimi smo imeli več opravka predvsem pri postavitvi v prostor, kjer smo pazili na morebitno prekrivanje predmetov. Asteroidom smo določili smer in hitrost premikov po prostoru. Ker tokrat še nismo sprogramirali detekcije trkov, asteroidi med premiki prehajajo drug skozi drugega. Detekcija trkov je pomemben del igre Asteroidi, saj bo morebiten trk z vesoljsko ladjo pomenil konec igre.

Prav tako bomo v prihodnjih člankih dodali streljanje izstrelkov vesoljske ladje in uničenje asteroida ob zadetku izstrelka ter vizualno eksplozijo ob pomoči senčilnikov, ki smo si jih v prejšnjih mesecih ogledali v tem vodniku. Vesoljski ladji bomo dodali možnost premikanja glede na uporabnikovo interakcijo, implementirali bomo tudi realistično vodenje vesoljske ladje z bočnimi izpuhi. Na koncu bomo dodali točkovanje in si ogledali enostavno izgradnjo menujev v igri.

Slika 4: Tako bo videti naša igra ob koncu. Morda boš ravno ti presegel rekord 41 milijonov točk v igri?

Slika 4: Tako bo videti naša igra ob koncu. Morda boš ravno ti presegel rekord 41 milijonov točk v igri?

Na koncu naj vas malce podražimo z videzom končane igre, katere razvoj opisujemo, prikazanim na sliki 4. Čeprav nas čaka še veliko dela, je aplikacija s tokratnim razvojem na dobri poti do končne oblike. Navdušenci, ki bi radi nadaljevali razvoj po lastni meri, lahko projekt Android Eclipse prenesete z naše spletne strani in dodate nove funkcionalnosti, ki jih podpirajo pametni telefoni, na primer premike ob pomoči nagibanja mobilne naprave, obračanje projekcije prostora na zaslon glede orientacije kompasa in uporaba drugih senzoričnih pripomočkov naprave.

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