Vesipisarapelin kolmas versio

Tutkitaan, miten vesipisarapeliin saa toteutettua graafisen käyttöliittymän Qt:lla. Pelin logiikka on lähes sama kuin edellisessä toteuttamassamme versiossa.

Ennen kuin alat tutkia hakemistosta examples/01-16/waterdrop_game_v3 löytyvää ohjelmakoodia tarkemmin, kannattaa jälleen suorittaa ohjelma ainakin kertaalleen. Jos näet vain ruudukon ilman vesipisaroita, niin valitse Qt:n vasemman reunan valikosta Projects ja poista rasti kohdasta Shadow build.

Vastuualueet

Ohjelma koostuu kahdeksasta luokasta ja pääohjelmasta. Käydään ensin läpi yleisellä tasolla, mitä mikäkin luokka tekee.

Tietosisältö ja pelilogiikka

Pelin tietosisältö ja pelilogiikka ovat luokkassa GameEngine. Tämän luokan attribuuttina on vector< vector< shared_ptr<Drop> > > board_, eli täsmälleen samanlainen pelilaudan sisältävä tietorakenne, joka pelin edellisessä versiossa oli toteutettuna.

Luokka GameEngine siis käyttää tietojen tallentamisessa apunaan luokkia Drop ja Splash. Tämä kolmen luokan kokonaisuus toimii hyvin samantyylisesti kuin ohjelman edellisessäkin versiossa. Täten Drop-olio sisältää tiedon pisaran vesimäärästä, ja siihen voi lisätä vettä. Kun vesimäärä tulee täyteen, pisara poksahtaa ja muodostaa neljä Splash-oliota. Edelleen Splash-oliot tietävät sijaintinsa ja suuntansa ja osaavat liikkua yhden ruudun verran oikeaan suuntaan.

Olennaisimpana erona edelliseen versioon on se, että GameEngine-olio sisältää myös osoittimen GameBoard-olioon, joka liittyy seuraavaksi käsiteltävään aiheeseen eli pelilaudan graafiseen esitykseen.

Graafinen esitys

Edellisessä versiossa vesipisaraolio ja roiskeolio tallensivat oman tulostusmerkkinsä, eli niiden vastuulla oli myös esitystapa. Tulostusmerkit ovat edelleen mukana Drop- ja Splash-olioissa. Edellisen version ascii-merkkipelilautaa on käytetty debug-tulosteena tämän version toteuttamisessa. Halutessasi saat debug-tulosteet näkyviin ottamalla kommenttimerkin pois metodin GameEngine::addWater riviltä 125, jolla suoritetaan print-metodin kutsu oliolle itselleen. Katso, mitä konsolissa tapahtuu, kun suoritat ohjelman debug-tulosteilla.

Graafisissa käyttöliittymissä tietosisältö on kuitenkin usein erotettu sen esityksestä. Esimerkiksi edellisellä ohjelmointikurssilla tästä tiedon ja sen esityksen erottamisesta keskusteltiin pohtimalla, mitä muita esitystapoja pelaajan pistemäärälle voisi graafisessa käyttöliittymässä olla kuin se, että käyttöliittymässä on label-olio, jossa pistemäärä näytetään numerona.

Olioihin Drop ja Splash liittyvät graafiset esitykset ovat luokissa DropItem ja SplashItem. Molemmat näistä luokista on periytetty luokasta QGraphicsPixmapItem. Luokan QGraphicsPixmapItem avulla voidaan käyttöliittymässä näyttää kuva, jota pystyy vaikkapa liikuttamaan helposti. Liikuttamiseen liittyvät ominaisuudet on toteutettu kantaluokassa valmiina. Periytetyllä luokalla on kaikki kantaluokan ominaisuudet, joten tällä tavoin toteutettuna SplashItem-olioita voi liikuttaa näytöllä, kun roiskeet lentävät pelilaudalla.

Graafisen esityksen sisältävällä DropItem-oliolla on osoitin vastaavan tietosisällön sisältävään Drop-olioon. Drop-olio sisältää tiedot pisaran tilasta, eli esimerkiksi sen, kuinka paljon vettä pisarassa on. DropItem-olio osaa näyttää kuvan oikean kokoisesta vesipisarasta, koska se kysyy Drop-oliolta sen vesimäärää osoittimen kautta. SplashItem-oliot toimivat samalla periaatteella.

Pelilautaa esittävä piirtoalue on toteutettu luokassa GameBoard. Tämä on siis luokan GameEngine tietosisällön graafinen esitys. Luokka GameBoard on periytetty luokasta QGraphicsScene, jossa voidaan näyttää graafisia piirtoelementtejä kuten QGraphicsPixmapItem-tyyppisiä olioita.

Olioilla DropItem ja SplashItem ei ole tietoa piirtoalueesta/pelilaudasta, eli ne vain näyttävät itsensä käyttöliittymässä eivätkä tee mitään muuta.

Luokka SplashAnimation liittyy kiinteästi yhteen luokkien Splash ja SplashItem kanssa. Palaamme tähän myöhemmin, kun tutkimme animaation toimintaa tarkemmin.

Kokonaisuus

Kuten jo kerrottiin, luokka GameEngine sisältää pelilogiikan. Sen lisäksi GameEngine sisältää ohjelman tilan kokonaisuudessaan. GameEngine-olio käskyttää kaikkia muita olioita ohjelman suorituksen aikana.

Pääohjelma luo ensin GameBoard-olion eli pelilautaa esittävän piirtoalueen ja sen jälkeen GameEngine-olion ja pääikkunan, joille piirtoalue annetaan parametrina.

Pääohjelmassa on myös rivi

QObject::connect(&scene, SIGNAL(mouseClick(int, int)), &engine, SLOT(addWater(int, int)));

Tällä rivillä yhdistetään piirtoalueen mouseClick-signaali pelimoottorin slotiin addWater. Tämä tarkoittaa, että kun graafisessa käyttöliittymässä klikataan kohtaa (x, y) lähettää piirtoalueolio signaalin, jonka vastaanottaa pelimoottori.

Signaalin ideahan oli, että lähettäjän ei tarvitse tietää, kuka signaalin vastaanottaa. Piirtoalueolio ei siis tiedä pelimoottorioliosta mitään.

Olioiden välinen kommunikointi

Edellistä ohjelmaversiota käsittelevässä Plussa-materiaalissa käytiin läpi, miten ohjelma toimii satunnaislukugeneraattorin siemenarvolla 7, kun poksautetaan vesipisara, joka sijaitsee koordinaateissa (4, 11). Emme kertaa Drop-olioiden ja Splash-olioiden välistä kommunikointia tässä, koska se tapahtuu täsmälleen samoin kuin pisaraolioiden ja roiskeolioden välinen kommunikointi edellisessäkin versiossa. Halutessasi voit kerrata edellisen version materiaalit nyt. Jos haluat asettaa siemenluvun graafisella käyttöliittymällä varustetussa ohjelmassa, vaihda tiedoston gameengine.hh riville 20 se siemenluku, jota haluat käyttää.

Kun käyttäjä klikkaa graafista käyttöliittymää hiirellä jossain kohdassa, lähettää GameBoard-olio siis signaalin mouseClick. Signaali aktivoi GameEngine-olion slotin addWater. Tutkitaan nyt ohjelmakoodista, miten ohjelma toimii tässä tilanteessa. (Avaa tiedosto gameengine.cpp ja etsi funktio addWater.)

Aivan ensimmäisenä pelimoottori vähentää tankissa olevan veden määrää. Riippuen siitä, lisättiinkö vettä tyhjään ruutuun vai ruutuun, jossa oli jo pisara, pelimoottori joko muodostaa uuden pisaraolion tai lisää vettä olemassa olevaan pisaraolioon. Pelimoottori pyytää GameBoard-oliota tekemään vastaavan muutoksen piirtoalueelle eli suorittaa joko metodin removeDrop tai addDrop.

Tämän jälkeen tulee do - while -rakenne, joka sisältää graafisen käyttöliittymän olennaisimmat toiminnot. Toistorakenteessa kutsutaan vuorotellen pelimoottorin omaa metodia moveSplashes ja GameBoard-olion metodia animate. Tämä on hyvin samantyylinen toimenpide kuin edellisen ohjelmaversion roiskeiden siirtäminen ja pelilaudan tulostaminen. Roiskeiden lento graafisessa käyttöliittymässä on siis toteutettu siten, että vaikka roiske lentäisi useamman ruudun pituisen matkan, niin lento toteutetaan useampana peräkkäisenä animaationa, joiden pituus on yksi ruutu.

Metodi moveSplashes palautti osoittimia pisaroihin, joiden koko kasvoi roiskeiden siirtymisen seurauksena. Samassa do - while -rakenteessa käydään myös läpi kaikki kasvavat pisarat. Tästä voi aiheutua uusia poksahduksia, jotka synnyttävät uusia roiskeita jne.

Kaksi eri koordinaatistoa

Luokan GameEngine attribuuttina board_ on samanlainen tietorakenne kuin pelin aikaisemmissakin versioissa. Tässä jokainen koordinaatti (x, y) vastaa täsmälleen yhtä vektorin alkiota eli yhtä ruutua pelilaudalla. Toisin sanoen pienin laudalta osoitettava yksikkö on yksi ruutu. Pelissä käytetään aina ruudun koordinaatteja, kun pelin tietosisältöä muokataan.

Graafisessa esityksessä pienin mahdollinen yksikkö on näytöllä näkyvä pikseli. Yhden ruudun näyttäminen suoraan yhden pikselin kokoisena ei ole järkevää, joten yhden ruudun näyttämiseen on valittu 50x50 pikselin alue. Esimerkiksi ruutu (0,0) kuvautuu alueelle, jota merkitään tässä px=[0,50], py=[0,50].

../../_images/koordinaatisto.png

Ruutu (5,4) kuvautuu alueelle, jota merkitään px=[200,250], py=[150,200].

Koska graafinen esitys on GameBoard-olion vastuulla, tämän on osattava tehdä muunnos ruudukon koordinaatesta (x, y) näytön pikselikoordinaatistoon (px, py) ja takaisin. GameBoard-oliossa pikselikoordinaatiston arvoja käsitellään aina käyttäen apuna QPoint-tyyppisiä olioita, jotka kuvaavat yksittäisen pikselikoordinaatin (px, py).

Laskut tehdään käyttäen apuna vakiota const int GRID_SIDE = 50. Esimerkiksi tiedoston gameboard.cpp funktiossa mousePressEvent käsitellään tilanne, jossa hiirellä on klikattu pelilaudan graafista esitystä näytöllä ja klikkauksen koordinaatit, joihin päästään käsiksi metodikutsuilla clickPosition.x() ja clickPosition.y(), ovat pikselikoordinaatteina (px, py). GameBoard-olio suorittaa jakolaskun clickPosition.x() / GRID_SIDE, jonka tuloksena C++:ssa on kokonaisluku (kokonaislukujen jakolasku), joka kertoo klikatun ruudun X-koordinaatin.

Laskut eivät ole vaikeita, mutta ainoa tapa saada kaikki elementit oikeille kohdilleen näytöllä, on piirtää kuva ja laskea kaikki sijainnit. Ohjelmoija tarvitsee siis matematiikkaa. Etenkin diskreetti matematiikka on hyödyllistä.

Animointi

Animointi käynnistetään metodissa GameEngine::moveSplashes, kun GameEngine-olio kertoo GameBoard-oliolle metodia addSplash käyttäen, että pitää lisätä animaatio pisteestä x pisteeseen y. GameEngine-olio ei tiedä, miten se tapahtuu, vaan se on kokonaan graafisen esityksen vastuulla.

GameBoard-oliolla on attribuuttina QParallelAnimationGroup-tyyppinen animaatioryhmä animations_. Tämä on ryhmä animaatioita, jotka suoritetaan rinnakkain tai toisin sanottuna yhtä aikaa. Tätä voisi verrata siihen, että edellisessä ohjelmaversiossa kaikki olemassa olevat roiskeoliot oli talletettuna vektoriin, ja niitä jokaista siirrettiin yksi pykälä eteenpäin samalla kertaa.

Metodissa GameBoard::addSplash luodaan uusi SplashAnimation-olio, joka lisätään animations_-ryhmään. Metodi GameBoard::animate käynnistää sitten kaikki animaatiot yhtä aikaa. Tämän jälkeen tulee while-rakenne, jossa odotetaan animaatioiden päättymistä. Animaatioiden eteneminen on sarja tapahtumia, joita QEventLoop (tapahtumankäsittelijä) käsittelee. (Tapahtumankäsittelijästä puhuimme lyhyesti jo edellisellä ohjelmointikurssilla, kun alettiin sekventiaalisen ohjelmoinnin jälkeen puhua tapahtumapohjaisesta ohjelmoinnista.)

Yhteenveto

Tässä materiaalissa ei käsitelty kaikkia ohjelmakoodin osia, ja ohjelma sisältää myös jonkin verran sellaista Qt:hen liittyvää ohjelmakoodia, jota ei tämän opintojakson taidoilla pysty täysin ymmärtämään. Silti graafisen käyttöliittymän toteutuksessa voidaan havaita yhtymäkohtia aiemmin toteuttamiimme vesipisarapelin versioihin. Ohjelman toimintalogiikassa on paljon tuttua jo niiden pohjalta.

Qt on erittäin suuri kirjasto, joka mahdollistaa monimutkaistenkin ohjelmien tehokkaan toteutuksen. Siihen perehdytään lisää seuraavalla ohjelmointikurssilla, mutta pelkästään näiden opintojaksojen oppimateriaalin pohjalta Qt:sta ei saa kuin pintapuolisen käsityksen. Tämä opintojakso tarjosi pohjan, jolta Qt:hen tutustuminen itsenäisesti on mahdollista aloittaa. Vesipisarapeli on jo kahdessa aikaisemmassa versiossa tullut tutuksi, joten toivottavasti Qt:n tutkiminen on sen avulla hiukan helpompaa.

Vesipisarapelin toteuttaminen kolmessa vaiheessa antoi myös esimerkin siitä, miten ohjelmistoja kehitetään vaiheittain. Neljännen, vielä hienomman version saat kehittää itse.