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].
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.