Projektiraportti

Hannu Hartikainen 19.1.2009

nbody

Nbody on gravitaatiosimulaatio, jonka painopiste on hyvässä käyttöliittymässä. Se simuloi mielivaltaista määrää massallisia kappaleita, joiden vetovoimat vaikuttavat kaikkiin muihin kappaleisiin. Kappaleiden (planeettojen) ominaisuuksia voi säätää reaaliaikaisesti.

Toiminnallisuus

Ohjelma pyörittää fysiikkamallinnusta, joka laskee kaikkien planeettojen väliset gravitaatiovuorovaikutukset ja niiden avulla simuloi planeettojen liikeratoja. Simulaatio on varsin realistinen ja tarkka; kokeilin tarkkuutta syöttämällä Maan ja Kuun parametrit ja saadut liikeradat noudattivat oikeita täsmällisesti (järkevällä tarkkuudella). Toki joissain erikoistilanteissa havaittavia epätarkkuuksiakin syntyy; tästä lisää fysiikkamallinnukselle omistetussa kohdassa.

Planeettojen määrää ei ole rajoitettu mitenkään; käytännössä koneen laskentateho asettaa jonkinlaisen rajoituksen. Esimerkiksi oma vuodelta 2006 peräisin oleva MacBookini jaksaa simuloida noin 50 planeettaa. Planeettoja voi lisätä ja poistaa reaaliaikaisesti simulaation ollessa käynnissä. Samoin planeettojen ominaisuuksia voi muuttaa lennossa.

Näkymää saa liikuteltua ja zoomattua vapaasti. Koska avaruudessa ei ole kiintopisteitä, on apuvälineenä mahdollista käyttää taustaruudukkoa.

Ohjelmassa on myös mahdollisuus maailmojen tallentamiseen ja lataamiseen; itse asiassa ohjelma yrittää käynnistyessään ladata aurinkokunnan tiedostosta.

Käyttöliittymä

Käyttöliittymän tärkeimmät osat ovat näkymäpaneeli ja sivupaneeli. Näkymäpaneelissa näytetään planeetat nimineen ja taustaruudukko, ja se päivittyy reaaliajassa (tai niin usein kuin Swing ehtii piirtää sen). Sivupaneelissa on kaksi osaa: Planet ja View. Planet-osassa näkyy valitun planeetan ominaisuudet: nimi, väri, sijainti, massa sekä nopeus; nekin päivittyvät reaaliajassa, ja niitä voi muokata. Lisäksi siinä on valikko planeetan valitsemiseen ja kaksi painiketta, joilla voidaan poistaa valittu planeetta tai kaikki muut planeetat. View-osassa näkyy näkymän ominaisuudet: näkymäpaneelin keskipisteen koordinaatit ja zoom-taso. Lisäksi siinä on Pause-nappi, jolla simulaation voi keskeyttää ja ruksit, joista valitaan näytetäänkö planeettojen nimet ja taustaruudukko.

Paneeleiden lisäksi ikkunassa on valikkorivi, jonka kautta voi luoda tyhjän maailman sekä ladata tai tallentaa maailman, poistua ohjelmasta ja avata about-ikkunan. Lisäksi ikkunan alalaidassa on tilarivi, joka antaa vinkkejä planeettojen lisäämiseen liittyen.

Kuva 1: Ohjelmaikkuna

Planeetat

Planeettoja voi lisätä tuplaklikkaamalla haluamaansa avaruuden kohtaa. Ohjelma asettaa uuden planeetan tähän paikkaan ja jää odottamaan nopeuden asettamista. Tässä välissä planeetan ominaisuuksia voi muokata sivupaneelista. Planeetan nopeus asetetaan klikkaamalla johonkin avaruuden kohtaan; tällöin planeetta saa sijaintinsa ja klikatun kohdan välisen vektorin nopeusvektorikseen. Selvemmin sanottuna planeetta saa sellaisen nopeuden, että se on täsmälleen sekunnin kuluttua klikatussa kohdassa (mikäli siihen ei vaikuta mikään voima).

Sivupaneelissa näytettävän planeetan voi valita sivupaneelin valikosta, klikkaamalla sivupaneelin Next planet -nappia tai klikkaamalla planeettaa näkymäpaneelissa (tämä voi olla vaikeaa jos planeetta liikkuu). Valitun planeetan nimeä, väriä, massaa ja nopeutta voi muokata sivupaneelista. Uuden planeetan oletusmassa asetetaan kirjoittamalla haluttu massa tekstilaatikkoon ja painamalla enter; tämän jälkeen valitun planeetan massa muuttuu ja kaikkien myöhemmin lisättävien planeettojen massaksi tulee annettu arvo.

Näkymä

Näkymän keskikohtaa ja zoom-tasoa voi säätää sivupaneelista, mutta intuitiivisempi tapa on säätää sitä hiirellä näkymäpaneelissa. Pyrin tekemään käyttöliitymästä samanlaisen kuin Google Mapsissa, poislukien tuplaklikkaus jolla lisätään planeettoja zoomaamisen sijaan. Näkymää siis voi liikuttaa painamalla hiiren napin pohjaan ja liikuttamalla, ja zoomia voi säätää hiiren rullalla (hiiren kursori pysyy samassa kohdassa avaruutta).

Kohdeohjelman teknisen toteutuksen kuvaus

Yleisperiaatteet

Pyrin noudattamaan nbodyn toteutuksessa hyvää ohjelmointityyliä siltä osin kuin sen tunnen. En tehnyt tietoisia kompromisseja koodin laadun suhteen. Toki inhimillisiä virheitä varmasti on.

Toteutusympäristö

Suurin osa ohjelmakoodista on tuotettu kirjoittamalla käsin Eclipsellä; testaukseen on käytetty Eclipsen debuggeria. Versionhallintaan käytettiin Subversionia. Mahdollisuus palata vanhaan toimivaan versioon toi vapautta koodin kirjoittamiseen: ei tarvinnut pelätä, että rikkoisi kaiken todella pahasti.

Koska komponenttien asetteleminen kirjoittamalla Swing-koodia on todella hankalaa, päädyin kysymään pääassistentilta lupaa graafisen työkalun käyttöön. Sain luvan, sillä ehdolla että automaattisesti generoitu koodi on selvästi erillään minun kirjoittamastani koodista. Olin aiemmin tutustunut NetBeansin Swing-käyttöliittymäeditoriin ja huomannut sen olevan todella mukava käytettävä. Niinpä tein käyttöliittymän asettelun sillä ja kirjoitin komponenttien toiminnallisuuteen liittyvän koodin käsin. Asetteluun käytetään swing-layout-kirjastoa. Kirjaston lisenssi on LGPL, joten sen käyttämisestä ei seuraa levittämisen kannalta ongelmia.

Alun perin tarkoitukseni oli käyttää jotain valmista pelikirjastoa, jotta säästäisin aikaa. Perehdyin sdljavaan ja JGameen, mutta totesin molemmat käyttökelvottomiksi. Myös LWJGL:ää vilkaisin, mutta totesin OpenGL:n opiskelun olevan turhan vaivalloista. Niinpä lopulta päädyin piirtämään käsin JPaneliin. Pelkäsin, että ohjelmasta tulee tällä tavalla tahmainen, mutta lopputulos olikin suorastaan loistava! Ohjelma pyörii muutaman vuoden takaisilla halpiskoneillakin vielä sulavasti, toisin kuin valitettavan monet muut reaaliaikaiset Swing-ohjelmat. Syynä pitäisin onnistunutta säietoteutusta ja ylipäätään fiksua ja siistiä koodia.

Luokkarakenne

Oheisessa kaaviossa näkyy paketit ja niiden sisältämät luokat, joista nbody koostuu. Javadocit pyrkivät olemaan hyvät ja kuvaavat, ja lisäksi vaikeimmissa kohdissa on muutakin kommenttia. Tässä kuitenkin lyhyet kuvaukset luokista paketeittain. Tein ohjelman periaatteessa dancek.nbody -pakettiin, mutta kaksi yleiskäyttöistä luokkaa laitoin omiin paketteihinsa.

Kuva 2: nbodyn luokkarakenne

dancek.nbody

Nbody

Pääohjelmaluokka, joka luo NbodyFramen ja siten käynnistää ohjelman.

NbodyFrame

Ohjelmaikkunaluokka. Sisältää NbodyPanelin, PlanetPanelin, JMenuBarin ja JLabelin. Sisäluokkana hiirikuuntelija, joka huolehtii näkymäpaneelin käyttöliittymästä.

NbodyPanel

Näkymäpaneeliluokka. Hoitaa näkymän (avaruus, planeetat, taustaruudukko) piirtämisen.

PlanetPanel

Sivupaneeliluokka. Näyttää valitun planeetan ja näkymän ominaisuuksia ja huolehtii niiden muokkaamisen, jos käyttäjä muuttaa jonkin komponentin arvoa. Komponenttien sijoittelu on tehty NetBeansin gui-editorilla. Käytännössä siis initComponents()-metodi on automaattisesti generoitua koodia; muun olen kirjoittanut itse.

PhysicalObject

Fyysistä kappaletta kuvaava luokka. Pääosin vain tietorakenne, mutta sisältää update(double dt)-metodin, joka päivittää kappaleen parametrit ajanhetken dt päähän. Kyseisessä metodissa on siis toteutettu liikeratojen integrointi.

Planet extends PhysicalObject

Planeettaa kuvaava luokka. Sisältää planeetalle ominaiset asiat; PhysicalObject on laajempi ja tärkeämpi luokka.

World

Maailmaa kuvaava luokka. Pitää listaa planeetoista; sisältää metodit resetAllForces, gravitateAll ja updateAll planeettojen välisten voimien laskemiseen ja kunkin planeetan päivitysmetodin kutsumiseen. Lisäksi sisältää metodit maailman tallentamiseen ja lataamiseen.

RenderingThread

Luokka, joka huolehtii fysiikkamallinnussäikeen luomisesta. Säie kutsuu maailmaolion päivitysmetodeja, käskee sivupaneelin arvoja päivittymään ja vihjaa Swingille että näkymäpaneeli kannattaa päivittää.

AboutDialog

About-ikkuna. Näyttää html-tiedoston, joka sisältää lyhyen kuvauksen nbodyn käytöstä ja asioista, jotka ohjelmassa voisivat olla paremminkin.

dancek.io

RegexFileFilter

Oma FileFilter-luokka, joka suodattaa tiedostoja regex-lauseen perusteella.

dancek.vecmath

SimpleVector

Yksinkertainen vektoriluokka; sisältää vain yksinkertaisimmat laskutoimenpiteet. Toimii vastaavasti kuin Java 3D -apiin kuuluva Vector2d. Tein tämän luokan itse, koska Java 3D ei kuulu perusjakeluun ja halusin kaikkialla toimivan web start -version nbodysta.

Fysiikkamallinnus

Fysiikkamallinnus pyörii omana säikeenään vakiopäivitysnopeudella, joka on oletuksena 60 kertaa sekunnissa (tätä voi säätää koodista). Jokaisen fysiikkapäivityksen jälkeen kutsutaan sekä näkymäpaneelin että sivupaneelin repaint()-metodia, jotta ne piirrettäisiin mahdollisimman usein muttei kuitenkaan kahta kertaa saman fysiikkasyklin aikana. Piirtopyynnöt menevät jonoon, josta Swing käsittelee ne automaattisesti kun ehtii.

Fysiikkamallinnuksen perusidea on yksinkertainen: lasketaan joka syklillä kuhunkin kappaleeseen vaikuttavien voimien resultantti, lasketaan resultantista kiihtyvyys ja muutetaan nopeutta kiihtyvyyden perusteella sekä paikkaa nopeuden perusteella. Käytännössä toteutuksen vaihteleva osa on se, miten nopeuden ja paikan muutos toteutetaan. Molemmissa joutuu integroimaan, ja analyyttista ratkaisua ei n:n kappaleen tapauksessa ole. Aluksi käytin Euler-integrointia, joka on yksinkertaisin ja intuitiivisin vaihtoehto, mutta perehdyttyäni tarkemmin asiaan vaihdoin Verlet-integrointiin. Verlet vaatii vakiopituisen aika-askeleen ja tiedon sijainnista edellisellä aika-askeleella. Tämä on hienoinen ongelma ensimmäisen aika-askeleen osalta (jolloin edellistä aika-askelta ei ole olemassakaan), ja toisaalta tilanteissa joissa nopeutta säädetään käsin kesken kaiken (jolloin edellisen aika-askeleen tiedot antavat vääriä tuloksia). Ratkaisin ongelman laskemalla edellisen aika-askeleen paikalle approksimaation derivoimalla nopeutta PhysicalObjectin setVelocity-metodissa.

Fysiikkamallinnus on käytännössä kolmessa luokassa: RenderingThread, World ja PhysicalObject. Renderöintisäie kutsuu maailmaolion metodeja. Ensin kutsutaan resetAllForces-metodia, joka nollaa kaikkiin planeettoihin vaikuttavat voimat. Sitten kutsutaan gravitateAll-metodia, joka huolehtii gravitaatiovoimien laskemisesta. Lopuksi updateAll-metodi kutsuu kunkin planeetan (PhysicalObjectilta perittyä) update-metodia, joka sijainnin seuraavana hetkenä.

Toteuttamassani fysiikkamallinnuksessa on tiettyjä havaittavia epätarkkuuksia, joita onneksi olen pystynyt huomattavasti vähentämään ensimmäisestä fysiikkamallinnuksen versiosta. Mikäli planeettaan pääsee vaikuttamaan hyvin suuri kiihtyvyys, se ehtii yhden aika-askeleen aikana liikkumaan niin kauas ettei painovoimakenttä enää pysty pitämään sitä. Planeetta siis karkaa avaruuteen ja energian säilymislakia rikotaan. Tämä tapahtuu käytännössä, jos planeetat menevät hyvin läheltä toisiaan. Tämä ongelma ei ole järkevästi ratkaistavissa tämän tyyppisessä simulaatiossa, sillä se johtuu suoraan numeerisesta laskutavasta.

Huomattavaa on myös, ettei nbodyssa ole törmäysmallinnusta. Törmäysmallinnus olisi mieletön ominaisuus tämän tyyppisessä simulaatiossa, sillä planeetat ovat oikeasti niin pieniä suhteessa keskinäiseen välimatkaansa, että niiden törmääminen on miltei mahdotonta. Planeetat piirretään nbodyssa epärealistisen suuriksi käytettävyyden nimissä (oikeasti niiden pitäisi useimmiten olla huomattavasti pikseliä pienempiä). Siksi välillä näyttää graafisesti, että ne törmäisivät ja menisivät toistensa läpi.

Java Web Start

Tein ohjelmasta myös Web Start -version. Tämä osoittautui todella fiksuksi, sillä ohjelman testaaminen erilaisilla laitteistoilla ja alustoilla helpottui huomattavasti. Web Start -versiossa maailman tallentaminen ja lataaminen ei toimi. Tämänkin olisi saanut korjattua tekemällä jar-paketista sähköisesti allekirjoitetun, mutta en kokenut tarpeelliseksi perehtyä asiaan. Web Start -ohjelman tekeminen oli yllättävän helppoa; kaikki toimi suoraan, kun vain teki sopivan jnlp-tiedoston.

Kokemukset projektista

Ajankäyttö

Stressistä jotain joskus oppineena totesin kurssin alkumetreillä, etten taatusti aio kirjoittaa Javaa joululomallani. Tämä päätös tietysti asetti hieman painetta syyslukukauteeni, varsinkin kun tosiasiassa aloitin projektin vasta 5.11. ja sain tehtyä marraskuussa vain 20 tuntia. Joulukuussa säikähdin vallitsevaa tilannetta: tunteja oli pelottavan vähän kasassa ja näytti etten muutenkaan ehtisi saada riittävästi aikaan annetussa ajassa. Siispä otin projektin tavoitteet uuteen tarkasteluun.

Alun perin tarkoituksenani oli tehdä luolalentelymäisiä elementtejä sisältävä peli, joka poikkeuksellisesti mallintaisi gravitaatiota kappaleiden välillä. Pian kuitenkin kävi ilmeiseksi, että peli jäisi projektin ajallisen lyhyyden vuoksi hyvin tylsäksi ja käyttöliittymältään nuivaksi. Päätin siis vaihtaa projektin tavoitetta hieman, ja tehdä vähemmän asioita paremmin. Rajasin kaikki pelimäiset ominaisuudet pois ja otin tavoitteekseni tehdä mahdollisimman käytettävän painovoimasimulaattorin. Tässä vaiheessa minulla oli jo yksinkertainen fysiikkamallinnus ja erittäin alkeellinen graafinen käyttöliittymä — oikeastaan vain näkymä. Saman päivän aikana minulle kehittyi visio sivupaneelista, johon planeettojen ominaisuudet päivittyisivät ja niitä voisi muuttaa reaaliaikaisesti.

Kun olin asettanut uudet tavoitteet, löysin varsin hyvän motivaation projektin jatkamiseen. Ensimmäiseen tenttiini mennessä olin kerennyt koodata 49 tuntia, ja tenttiviikolla oli varsin hyvin aikaa koodata lisää. Ohjelma oli valmis 20.12. ja koodi palautuskuntoista siistinnän ja bugikorjausten jälkeen 22.12. Tässä vaiheessa aikaa oli kulunut 74 tuntia. Toki muutama haluamani feature jäi toteuttamatta, mutta johonkin pitää lopettaa.

Kaiken kaikkiaan olen ajankäyttööni tyytyväinen. Tein aikataulun, jonka mukaan projektin oli tarkoitus olla valmis 14.12. juuri siksi, että tiesin aikataulun venyvän muiden hommien alla. Saavutin kuitenkin tärkeimmät tavoitteeni: sain projektin valmiiksi ennen joulua ja siihen mitoitetun ajan puitteissa. Raportin kanssa lopullinen ajankäyttö on noin 85 tuntia, mutta käyhän tämä harrastuksestakin...

Ongelmat

Suurimmat ongelmat projektissa liittyivät suunnitteluun. Kovin pahoihin bugeihin en törmännyt. Bugeista ehkä pahin johtui siitä, että olin ymmärtänyt Verlet-integroinnin väärin ja tein laskutoimituksia väärässä järjestyksessä.

Suurin suunnitteluun liittyvä ongelma oli taustasäikeen toteutus, varsinkin kun Swing ei ole säieturvallinen vaan kaikki mahdolliset ongelmat joutuu huomioimaan ihan itse. Sain selville, että repaint()-kutsut ovat turvallisia. Niiden lisäksi kuitenkin joudun muuttamaan komponenttien arvoja taustasäikeestä. Käytännössä tein tarkistuksia huolehtiakseni, ettei käyttäjä säädä komponenttia silloin kun sen arvoa muutetaan.

Yhteenveto

Projekti tarjosi reippaasti haastetta, mutta onnistuin ratkaisemaan vastaan tulleet ongelmat. Palauttamani ohjelma ei ole missään mielessä kompromissi, vaikka toki vielä muutaman featuren olisi voinut toteuttaa.

Mielestäni nbodyn käyttöliittymä on todella hyvä ja ohjelmaa on mukava käyttää. Luulin että Java asettaisi rajoitteita esimerkiksi animaation sulavuuteen ja käyttöliittymän responsiivisuuteen. Nämä kuvittelemani rajat kuitenkin ylitin reippaasti.

Projektin aikana opin monia yksittäisiä asioita. Esimerkiksi tietyt Swing-komponentit tulivat hyvin tutuiksi. Opin myös paljon siitä, miten fysiikkasimulaatioita (esim. peleihin) toteutetaan, ja mitkä ovat eri tapojen vahvuudet ja heikkoudet. Sain myöskin pintaraapaisun käyttöliittymien suunnitteluun ja toteutukseen (testautin väliversioita ja sain palautetta niistä). Todella suuria ahaa-elämyksiä projekti ei tarjonnut, mutta pieniä kyllä. Parasta oli, kun jatkuvasti koki onnistuvansa saadessaan toteutettua ominaisuuksia, jotka periaatteessa ovat melko vaikeita.

Kaiken kaikkiaan olen erittäin tyytyväinen tuotokseeni. Olen myös iloinen siitä, että kurssin puitteissa oli pakko tehdä näinkin laaja projekti. Omin päin tällaiseen ei olisi tullut ryhdyttyä, mutta nyt on jotain näytettäväksi potentiaaliselle työnantajallekin!

Ohjelmakoodi JavaDoc-kommentoituna

JavaDoc, ohjelmakoodi