Vesipisarapelin toinen versio¶
Vesipisarapelin ensimmäisessä versiossa pelilaudalla olevat vesipisarat poksahtaessaan roiskuttivat vettä toisiin vesipisaroihin, ja tavoitteena oli saada pelilauta tyhjennettyä vedestä. Jatkamme nyt vesipisarapelin parissa ja toteutamme siitä uuden version käyttäen uusia oppimiamme asioita kuten osoittimia.
Ensimmäisessä versiossa vesipisaroiden välinen kommunikointi toimi siten, että pisaran poksahtaessa ruutuolio lisäsi suoraan vettä niihin ruutuolioihin, joihin vesi poksahduksen seurauksena roiskahtaisi. Tämä ei kuitenkaan täysin vastannut esikuvana olleen peliversion toimintaa, koska siinä roiskeet siirtyvät pelilaudalla eteenpäin ruutu kerrallaan. Tällöin roiske saavuttaa viereisen ruudun nopeammin kuin pelilaudan toisella puolella olevan ruudun, mikä vaikuttaa esimerkiksi pisaroiden poksahtamisjärjestykseen.
Pelin moduulit ja tietorakenteet¶
Uudessa versiossa toteutamme pelin siten, että pelilaudalla ei ole
ruutuolioita, vaan vesipisaraolioita. Erona on se, että ensimmäisessä
versiossa jokaisessa pelilaudan ruudussa oli olio. Nyt toteutamme
version, jossa jokaisessa ruudussa on osoitin. Jos ruutu sisältää
vesipisaran, vesipisaraolio on talletettuna osoittimen päähän. Jos
ruutu on tyhjä, osoitin on nullptr
.
Uudessa versiossa on pisaraolioiden lisäksi roiskeolioita, jotka liikkuvat pelilaudalla. Poksahtava vesipisaraolio muodostaa pelilaudalle neljä roiskeoliota, jotka etenevät laudalla ja kuljettavat lisättävän veden siihen kohtaan lautaa, jossa se lisätään toiseen vesipisaraolioon. Uudessa versiossa on siis enemmän olioita, jotka kommunikoivat keskenään. Modulaarisuuden lisäksi saamme esimerkin olio-ohjelmoinnista.
Aivan kaikkea toiminnallisuutta on haastava toteuttaa pelkästään pisaraolioon ja roiskeolioon. Tarvitaan myös moduuli, joka vastaa pelin toiminnasta kokonaisuutena. Tämän moduulin nimeämme pelilaudaksi. Lopputuloksena on seuraavanlainen moduulijako:
- Luokka
Drop
toteuttaa yksittäisen vesipisaran, eli se sisältää esimerkiksi pisaran vesimäärän ja vastaa pisaran poksahtamisesta oikeaan aikaan. - Luokka
Splash
toteuttaa yksittäisen roiskeen, eli se sisältää tiedon siitä, missä kohdassa roiske on ja osaa edetä oikeaan suuntaan. - Edellisistä poiketen luokasta
GameBoard
luodaan vain yksi olio, joka hallinnoi pisaraolioita ja roiskeolioita. - Pääohjelmamoduuli muodostaa pelilautaolion ja vastaa käyttäjän syötteiden käsittelemisestä.
Ennen kuin alat tutkia hakemistosta examples/01-16/waterdrop_game_v2
löytyvää ohjelmakoodia tarkemmin, kannattaa suorittaa ohjelma ainakin
kertaalleen, joten sinun pitää kopioida se omaan student
-hakemistoosi.
Ohjelmakoodin toimintaa on helpompi ymmärtää, kun olet
nähnyt, miten toiminta esitetään ascii-grafiikkana näytöllä.
Olioiden välinen kommunikointi¶
Ohjelma on monimutkainen, eikä sitä ole välttämätöntä tutkia kokonaisuudessaan. Olio-ohjelmointinäkökulman ymmärtämiseksi käymme läpi yhden vesipisaran tipauttamista pelilaudalle.
Syötä satunnaislukugeneraattorille siemenarvoksi 7 ja sitten ensimmäisen vesipisaran koordinaateiksi 4 11. Tutki ohjelman tulostetta ja ohjelmakoodia selvittääksesi, mitä ohjelman suorituksessa tapahtuu. Ohjelmakoodista pitäisi löytyä seuraavat kohdat:
Funktio
main
kutsuu pelilaudan metodiaaddWater
, joka havaitsee, että ruudusta (4, 11) löytyy osoitin, jonka päässä on pisaraolio. Pelilautaolio kutsuu kyseisen pisaraolionaddWater
-metodia.Koordinaateissa (4, 11) sijaitseva pisara poksahtaa. Se muodostaa neljä roiskeoliota, joiden kaikkien sijaintina on ruutu (4, 11), mutta joista jokaisella on eri suunta. Metodille
addWater
on välitetty pelilaudansplashes_
-vektori viiteparametrina, joten metodin suorittamatpush_back
in kutsut kohdistuvat tähän vektoriin, eli uudet roiskeoliot menevät suoraan pelilaudalle.Funktio
main
kutsuu pelilaudan tulostusmetodia, joka ensi töikseen kutsuu metodiamoveSplashes
.Metodi
moveSplashes
käy läpi kaikki pelilaudansplashes_
-vektorissa olevat roiskeoliot ja kutsuu jokaiselle näistämove
-metodia.Ylös- ja alaspäin menossa olevat roiskeet siirtyvät ruutuihin, joissa on vesipisara. Metodi
moveSplashes
huomaa tämän ja kutsuu metodiaaddWater
oikealle pisaraoliolle ja tuhoaa roiskeolion. Tämän vaiheen voit havaita seuraavassa tulostuvassa pelilautakuvassa siten, että ruudun (4, 11) ylä- ja alapuolella olevat pisarat ovat kasvaneet yhtä pykälää isommiksi.Sen sijaan vasemmalle ja oikealle päin menossa olevat roiskeet päätyvät ensimmäisen
move
-metodikutsunsa jälkeen ruutuun, jossa onnullptr
sen merkiksi, että ruudussa ei ole pisaraoliota. Pelilaudan tulostuksessa tämä näkyy siten, että roisketta kuvataan merkeillä<
ja>
.Pelilaudan tulostaminen on monimutkaisempaa, kuin kierroksella 4 toteutetussa ohjelmassa, koska tyhjää ruutua ei voi suoraan tulostaa välilyöntimerkillä, vaan ensin pitää selvittää, onko ruudussa roiske tai jopa useampia roiskeita. Tyhjän ruudun tulostusmerkin valitseminen on toteutettu omassa funktiossaan, jotta tulostusfunktio olisi selkeämpi. Todennäköisesti ymmärrät ohjelman toiminnan ilman metodin
droplessSquareChar
tutkimistakin.Pelilaudan tulostusfunktio toimii siten, että tulostusta toistetaan
do - while
-rakenteessa niin kauan, kuinsplashes_
-vektorissa on jäljellä yksikin roiske.Pelilaudan yksi roiske etenee ruutuun (2, 11) asti siten, että ennen jokaista tulostusta pelilaudan tulostusmetodi kutsuu metodia
moveSplashes
. Sama pätee myös roiskeelle, joka etenee ruutuun (6,11), mutta keskitytään tässä vaiheessa vain ensiksi mainittuun roiskeeseen, joka liikkuu vasemmalle.Kun roiske on ruudussa (2, 11), metodi
moveSplashes
huomaa jälleen tämän ja lisää vettä kyseisessä ruudussa olevaan pisaraolioon senaddWater
-metodia käyttäen sekä tuhoaa roiskeolion.Veden lisääminen ruudussa (2, 11) olevaan pisaraolioon saa myös aikaan poksahduksen, eli pisaraolio muodostaa taas neljä uutta roiskeoliota. Metodi
moveSplashes
ei kuitenkaan välittänytaddWater
-metodille parametrina pelilaudansplashes_
-vektoria, vaan toisen väliaikaissäiliön. Siksi nämä roiskeet eivät ala heti siirtyämoveSplashes
-metodin toistorakenteessa. Viimeisenä toimenaanmoveSplashes
tallentaa uudet muodostuneet roiskeet pelilaudansplashes_
-vektoriin, minkä vuoksi pelilaudan tulostus jatkuu.Seuraavan kerran pelilautaa tulostettaessa laudalla on siis neljä roisketta ruudussa (2, 11). Tulostusfunktio kutsuu metodia
droplessSquareChar
, joka palauttaa ruudun (2, 11) kohdalla “sotkumerkin”*
, koska yhteen ruutuun ei voi tulostaa neljän erisuuntaan menossa olevan roiskeen merkkejä. Tämä “sotkumerkki” siis kuvaa tavallaan myös vesipisaran poksahtamista.Pelilaudan tulostus jatkuu taas siten, että ensin tulostusfunktio kutsuu metodia
moveSplashes
. Roiskeiden siirtyessä jo läpi käydyllä tavalla osuu kolme roiskeista suoraan vesipisaroihin, ja oikealle lähtenyt roiske tyhjään ruutuun.Hyvin samanlaista tapahtuu ruudussa (6,11). Erona on tosin se, että nyt vain kaksi roisketta osuu pisaroihin, nimittäin ylös ja oikealle etenevät. Alas ja vasemmalle etenevät roiskeet osuvat tyhjään ruutuun.
Kun oikealle etenevä roiske (ruudusta (2, 11)) ja vasemmalle etenevä roiske (ruudusta (6, 11)) ovat yhtä aikaa ruudussa (4, 11) tulostuu jälleen “sotkumerkki”
*
, joka ei tällä kertaa esitä uutta poksahdusta, vaan sitä, että eri suuntiin etenevät pisarat olivat hetken samassa ruudussa.
Samaan tyyliin roiskeet jatkavat laudalla etenemistä, kunnes muutaman pelilaudan tulostuksen jälkeen kaikki roiskeet ovat kadonneet.
Pisaraolioiden ja roiskeolioiden vastuut¶
Edellistä toimintaesimerkkiä tutkiessasi huomasit, että ohjelman rakenne on melko monimutkainen. Herää kysymys, että oliko tästä olioiden paljoudesta nyt jotain hyötyä. Olisiko kuitenkin ollut parempi, että pelilauta olisi vain sisältänyt tiedot pisaroiden vesimääristä ja roiskeiden sijainneista, ja tämä pelilautaolio olisi sitten hallinnut koko toimintaa?
Roiskeiden kohtaaminen¶
Yhtenä esimerkkinä siitä, mitä hyötyä on pienestä asiasta vastuun
ottavasta oliosta, on se kohta edellistä suoritusta, jossa kaksi
roisketta etenevät päinvastaisiin suuntiin ja kohtaavat toisensa
samassa ruudussa.
Kun katsot luokkaa Splash
ja mietit, miten tämä
kahden (tai useammankin) roiskeen kohtaaminen monimutkaistaa roiskeen
liikkumisalgoritmia, niin huomaat, että ei mitenkään.
Splash
-luokan vastuulla on yksittäisen roiskeolion liikkuminen.
Vaikka kaksi oliota kohtaisivatkin samassa ruudussa, ei se
monimutkaista yksittäisen roiskeen toimintaa.
Jos meillä olisi tämän sijaan pelilautaolio, joka hallinnoisi kaikkia
pisaroihin ja roiskeisiin liittyviä yksityiskohtia, vaikuttaisi tämä asia
ehkä roiskeiden siirtämisalgoritmiin jollain toisella tavalla.
Olio-ohjelmoinnin ideana siis oli muodostaa pieniä osia, jotka vastaavat
omasta toiminnastaan itsenäisesti.
Olioiden tuhoutuminen¶
Edellä kävimme jo läpi, miten pelilautaolio huolehtii roiskeolioiden
tuhoamisesta metodissa moveSplashes
. Sen sijaan pisaraolioiden
tuhoutuminen tapahtuu hyvin erityylisesti. Jos pisaraolion metodissa
addWater
käy niin, että pisaran maksimikoko ylittyy ja se
poksahtaa, tuhoaa pisaraolio itse itsensä roiskeiden muodostamisen
jälkeen. Tämä tapahtuu siten, että pisaraolio kutsuu pelilautaolion
metodia removeDrop
.
On tärkeää, että tämä itsetuhoinen toimenpide on ehdottomasti viimeinen asia, mitä metodissa tapahtuu, koska olion toiminta sen jälkeen, kun sille varattu muisti on vapautettu, on määrittelemätöntä.
Tämä ero roiskeolioiden tuhoamisessa ja pisaraolioiden
tuhoutumisessa kuvastaa hyvin sitä, mitä tarkoitetaan, kun puhutaan
moduulin vastuualueesta.
Pisaraolio on omatoiminen tuhoutumisessaan.
Tämä helpottaa pelilautaolion toteuttamista.
Sen sijaan pelilautaolion moveSplashes
-metodissa suoritetaan aika
paljon asioita, jotka voisivat hyvin kuulua roiskeolion vastuulle.
Toisenlaisessa toteutuksessa roiskeolio voisi suoraan lisätä veden
siihen pisaraolioon, johon se törmää, ja poistaa itse itsensä.
Koska pelilautaolio suorittaa joitakin roiskeiden toimenpiteitä, on se
paisunut suuremmaksi kuin ehkä on hyvä.
Toisaalta, jos roiskeoliolle olisi laitettu enemmän vastuuta, tarvitsisi sekin
pääsyn käsiksi pelilautaan, mikä monimutkaistaisi ohjelman tietorakenteita.
Jokaisessa erilaisessa ratkaisussa on monta eri näkökulmaa.
Ei ole yhtä oikeaa ratkaisua, vaan ohjelmoijan pitää osata punnita eri
ratkaisujen hyviä ja huonoja puolia.
Yhteenveto¶
Epätäydellisyydessään tämä ohjelman toteutus tarjoaa siis hyvän mahdollisuuden filosofiseen pohdintaan siitä, mikä toteutuksen moduulijaossa on hyvää ja mikä huonoa. Onko moduulien vastuualueet valittu järkevästi? Vastaako jokainen moduuli niistä asioista, mitkä sen vastuulle kuuluvat? Jos vastuiden jakoa muutettaisiin, yksinkertaistaisiko vai monimutkaistaisiko se ohjelmaa?
Näihin kysymyksiin perehdytään enemmän tätä opintojaksoa seuraavilla opintojaksoilla (esimerkiksi Ohjelmointi 3: Tekniikat sekä Ohjelmistojen suunnittelu). Mutta jos olet kiinnostunut aiheesta, kannattaa sitä pohtia omia projekteja tehdessä muutenkin.