Luku 3.1: Vuorovaikutteista grafiikkaa

../_images/person05.png

Johdanto

Tässä luvussa saadaan ötökkä liikkeelle ja muutakin mukavaa. Mutta aloitetaan yksinkertaisesta esimerkistä.

Lukumäärälaskuri

Osassa tämän luvun ohjelmista tarvitsemme yksinkertaista laskuria, jolla voi pitää kirjaa mistä tahansa yksittäin kasvavasta lukumäärästä, vaikkapa käyttäjän tekemistä klikkauksista.

Tässä luodaan nollasta alkava laskuri, jonka arvo kasvaa aina yhdellä kerrallaan, kun laskurin vaikutuksellista etene-metodia kutsutaan:

val ekaLaskuri = Laskuri(0)ekaLaskuri: Laskuri = arvo 0
ekaLaskuri.arvores0: Int = 0
ekaLaskuri.etene()ekaLaskuri.arvores1: Int = 1
ekaLaskuri.etene()ekaLaskuri.etene()ekaLaskuri.arvores2: Int = 3

Alkuarvo voi olla muukin kuin nolla:

val tokaLaskuri = Laskuri(100)tokaLaskuri: Laskuri = arvo 100
tokaLaskuri.etene()tokaLaskuri.etene()tokaLaskuri.etene()tokaLaskuri.arvores3: Int = 103

Kuvatulla tavalla toimiva laskuriluokka on helppo toteuttaa aiemmista luvuista tutuin konstein:

class Laskuri(var arvo: Int):

  def etene() =
    this.arvo = this.arvo + 1

  override def toString = "arvo " + this.arvo

end Laskuri

Uusi muuttujan rooli: askeltaja

../_images/stepper.png

Askeltajan "reitti" on ennalta määrätty.

Laskuriluokan arvo-ilmentymämuuttujalla on hieman erilainen rooli kuin millään muulla tähän mennessä kohdatulla muuttujalla (vrt. luvun 2.6 roolit). Sen arvo kasvaa alkuarvosta yksi kerrallaan. Sekvenssi on samalla alkuarvolla aina sama, esimerkiksi 0, 1, 2, 3 jne.

Askeltaja (stepper) on muuttuja, jonka arvo "astelee" tiettyä sekvenssiä pitkin. Arvosekvenssi on ennalta määrätty, kunhan tiedetään lähtötilanne. Usein kyseessä ovat kokonaisluvut, joita käydään läpi nousevassa tai laskevassa järjestyksessä yksi kerrallaan, mutta askellettava sekvenssi voi olla jokin muukin.

Askeltajat ovat ohjelmissa yleisiä. Laskuriluokassa nähtiin eräs tyypillinen käyttötarkoitus askeltajalle: halutaan pitää kirjaa jostakin kasvavasta lukumäärästä. Toinen tyypillinen käyttö — järjestysnumerojen käsittely — tulee vastaan myöhemmin kurssilla.

Kuten kokoojan myös askeltajan uusi arvo riippuu sen vanhasta arvosta. Kuitenkaan askeltajan seuraava arvo ei riipu ulkoisista tekijöistä (esim. annetuista syötteistä) vaan seuraa aina tiettyä ennalta määrättyä sekvenssiä.

Ohjelmani huomaa, kun klikkaan

Tehdään kokeeksi ohjelma, joka osaa laskea klikkauksia ja tehdä näkyviä muutoksia näkymään sitä mukaa kun klikkauksia kertyy.

Käytetään mallina yhtä Laskuri-oliota. Kirjoitetaan käyttöliittymä, joka piirtää ruudulle sitä suuremman ympyrän, mitä useammin käyttäjä on näpäyttänyt hiiren nappia.

Muodostetaan aluksi graafinen näkymä ja käynnistysfunktio käyttäen luvun 2.7 jo esittelemiä keinoja:

val klikkauslaskuri = Laskuri(5)

val sininenTausta = rectangle(500, 500, Blue)

object klikkausnakyma extends View("Klikkauskokeilu"):
  def makePic = sininenTausta.place(circle(klikkauslaskuri.arvo, White), Pos(100, 100))
end klikkausnakyma

@main def kaynnistaKlikkausohjelma() =
  klikkausnakyma.start()

Mallina on vain yksi laskuriolio, joka arvo alkaa vaikkapa viitosesta.

makePic-metodi muodostaa kuvan asettamalla taustan päälle tiettyyn kohtaan valkoisen ympyrän, jonka halkaisija on laskurin arvon mittainen (eli viisi pikseliä).

Jotta saamme käyttöliittymämme reagoimaan hiiren näpäyksiin, meidän on määriteltävä sille tapahtumankäsittelijä (event handler). Tapahtumankäsittelijä on koodin osa, joka suoritetaan, kun havaitaan uusi tapahtuma (event); tapahtumia ovat esimerkiksi hiiren klikkaukset ja liikkeet sekä näppäimen painallukset.

Täydennetään klikkausnakyma-oliotamme lisäämällä sille metodi, joka toimii tapahtumankäsittelijänä:

object klikkausnakyma extends View("Klikkauskokeilu"):

  def makePic = sininenTausta.place(circle(klikkauslaskuri.arvo, White), Pos(100, 100))

  override def onClick(klikkauskohta: Pos) =
    klikkauslaskuri.etene()
    println("Klikkaus koordinaateissa " + klikkauskohta + "; " + klikkauslaskuri)

end klikkausnakyma

Tässä tapahtumankäsittelijänä toimii metodi nimeltä onClick. Voit ajatella asiaa niin, että View-oliot osaavat kytätä itseensä kohdistuvia tapahtumia ja kutsua tätä omaa metodiaan klikkauksen sattuessa. Katsotaan metodia kohta tarkemmin.

Löydät ohjelman Pikkusovelluksia-moduulin pakkauksesta o1.laskuri. Aja ohjelma, napsauttele hiirellä ja havainnoi mitä tapahtuu. Katso sekä graafista ikkunaa että tekstikonsolia.

Nyt kun olet kokeillut ohjelmaa, tutkitaan tapahtumankäsittelijämetodia:

override def onClick(klikkauskohta: Pos) =
  klikkauslaskuri.etene()
  println("Klikkaus koordinaateissa " + klikkauskohta + "; " + klikkauslaskuri)

Jos haluamme reagoida hiiren napsautuksiin, on meidän syytä nimetä metodi täsmälleen näin ja kirjata parametrin tyypiksi Pos. Tällöin GUI-kirjastomme osaa kutsua oikeaa metodia tapahtuman havaitessaan ja välittää sille parametriksi tiedon koordinaateista, joissa klikkaus havaittiin.

Metodin runko määrittelee, mitä haluamme tämän sovelluksen tekevän klikkauksen sattuessa. Tässä tapauksessa komennamme laskuriamme kirjaamaan yhden klikkauksen ja tulostamme vielä tekstimuotoisen raportin konsoliin.

Monet tapahtumankäsittelijät saavat parametrina lisätietoja tapahtumasta. Tässä lisätietona ovat hiiren koordinaatit, joilla tämä sovelluksemme ei tee muuta kuin tulostaa ne.

Parametrimuuttujan voi nimetä vapaasti, vaikka suomeksi. (Tässäkin luvussa on paljon "suomenkielistä koodia", jotta korostuu, mikä on osa valmista kalustoa ja mikä nyt itse tekemäämme. Kurssin edetessä nimeämme kasvavassa määrin englanniksi.)

Metodin alkuun tulee kirjata override samaan tapaan kuin olemme jo tottuneet tekemään toString-metodille (luku 2.5). View-luokassa on nimittäin määritelty valmiiksi erilaisia tapahtumankäsittelijämetodeita kuten onClick. Ne tosin jättävät tapahtumat huomiotta, koska tapa, jolla tapahtumiin tulisi reagoida ei ole yleisesti määriteltävissä vaan riippuu sovelluksesta. Tässä korvaamme tuon mitään tekemättömän oletustoteutuksen sovelluskohtaisella metodillamme.

Huomaa, että meidän ei erikseen tarvinnut kutsua onClick-metodia missään, vaan se tulee meidän näkökulmastamme automaattisesti kutsutuksi käyttäjän napsauttaessa hiirellä. Tämän automatiikan meille tarjoaa View-luokka: kukin View-tyyppinen olio osaa toimia ns. tapahtumankuuntelijana (event listener), joka saa ilmoituksen itseensä kohdistuvista tapahtumista ja kutsuu tapahtumankäsittelijämetodeita kuten onClick.

Ohjelmani huomaa, kun näppäilen

Tapahtumankäsittelijä näppäimille

Näppäimenpainallukseen voi reagoida ihan vastaavasti kuin hiirenklikkaukseen. Tarvitsemme vain hieman erilaisen tapahtumankäsittelijämetodin:

override def onKeyDown(painettu: Key) =
  println("Painettiin näppäintä " + painettu)

Tapahtumankäsittelijän tulee nyt olla nimeltään onKeyDown. Parametriksi saadaan Key-tyyppinen arvo. Eri Key-oliot vastaavat näppäimistön eri näppäimiä.

Tämä tapahtumankäsittelijä vain tulostaa tiedon siitä, mitä näppäintä painettiin.

FlappyBug-tehtävä (osa 5/17: ötökkä liikkuu)

Palataan FlappyBug-ohjelmaan. (Jos et tehnyt noita tehtäviä viime kierroksella, käy aiemmat vaiheet nyt läpi. Voit käyttää niiden esimerkkiratkaisuja, jotka löytyvät linkeistä tehtävien kohdalta määräajan jälkeen.)

Ota esiin luvussa 2.7 tehty versio FlappyBug-käyttöliittymästä. Sen flappyView osaa jo tuottaa kuvan pelimaailmasta muttei muuta.

  1. Lisää flappyView-oliolle yllä annettu onKeyDown-tapahtumankäsittelijä. Aja ohjelma ja totea tekstikonsolin raportoivan näppäimenpainallukset.

  1. Poista tulostuskäsky tapahtumankäsittelijästä. Kutsu sen sijaan Game-olion activateBug-metodia. Nyt siis (minkä tahansa) näppäimen painalluksen pitäisi aktivoida peliin sisältyvä ötökkä.

  2. Aja muokattu ohjelma. Huomaa:

    • Ötökkä liikkuu!

    • Ötökkä lentää muutaman painalluksen jälkeen ulos näkyvältä pelialueelta.

    • Ötökkä ei putoa lainkaan, eikä este liiku. Aika ei kulu pelimaailmassa. Et ole vielä kutsunut Game-olion timePasses-metodia. (Tehdään se kohta.)

  3. Palauta ratkaisusi.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Ohjelmani tikittää

Tapahtumien ei välttämättä tarvitse seurata suoraan käyttäjän tekemisistä. Eräänlainen tapahtuma on sekin, että hetki kuluu.

Tikitysohjelma

Pikkusovelluksia-moduulin pakkauksesta o1.laskuri löytyy klikkausohjelmaa muistuttava ohjelma, jossa laskemme klikkausten sijaan "kellonviisarin naksahduksia". Aja se ja katso alta selitys sen koodista.

val tikityslaskuri = Laskuri(0)
val tausta = rectangle(500, 500, Black)

object nakyma extends View("Tikittävä ohjelma"):

  def makePic = tausta.place(circle(tikityslaskuri.arvo, White), Pos(250, 250))

  override def onTick() =
    tikityslaskuri.etene()

end nakyma

@main def kaynnistaTikitysohjelma() =
  nakyma.start()

onClick-metodin sijaan kirjoitimme onTick-metodin. Se on parametriton.

View-olio käyttää ajastinta, joka synnyttää "kellon naksahduksia" suunnilleen 24 kertaa sekunnissa. Naksahduksen havaitessaan se kutsuu onTick-tapahtumankäsittelijää ja laskurimme arvo kasvaa.

Tehtävä: tikitystä ja pyöritystä

Muokkaa äskeisen ohjelman makePic-metodia niin, että:

  • Käyttäjää "lähestyvä" kuvio ei olekaan laskurin kokoinen ympyrä vaan neliö, jonka sivut ovat laskurin mittaisia.

  • Neliö paitsi kasvaa myös pyörii yhden asteen verran myötäpäivään per tiksahdus. Käytä pyörittämiseen clockwise-metodia luvusta 2.3, ja anna sille parametriksi laskurin arvo.

Kokeile ohjelmaa. Kokeile sitten vielä muuttaa tikitysnopeutta: anna View’lle toiseksi luontiparametriksi luku 50. Nyt onTick-metodi tulee kutsutuksi noin viisikymmentä kertaa sekunnissa, ja kuvio suurenee ja pyörii nopeammin.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lisäkokeiluja

Kokeile muita nopeuksia. Isommalla vauhdittuu, pienemmällä hidastuu. Nollan ja ykkösen välisellä nopeudella kello tikittää harvemmin kuin kerran sekunnissa.

Kokeile vaihtaa neliön paikalle jokin muu kuva. Skaalaamalla pyörivän face.png-kuvan aina vain suuremmaksi saa aikaan melko häiritsevän animaation. Tiedostosta ladatun kuvan koon säätämiseen voit käyttää joko luvun 2.3 esittelemää scaleBy-metodia tai vaihtoehtoisesti scaleTo-metodia, jolle annetaan parametreiksi kuvan haluttu leveys ja korkeus pikseleinä.

Kuka kutsuu makePiciä ja milloin?

View ilmeisesti päivittyy itsekseen, kun kerran kuvat liikkuvat ikkunassa ilman, että kutsun makePiciä...

Ajetaanko metodi makePic aina uudestaan, kun minkä tahansa onJokuToiminto() suoritus päättyy, esim. onClick tai onTick?

View-olion toimenkuvaan kuuluu, että se päivittää näkyvää grafiikkaa sen mukaan, kun sen tapahtumankäsittelijämetodeita kutsutaan.

Kunkin tapahtuman tapahduttua View-olio kutsuu omaa makePic-metodiaan ja pistää makePicin palauttaman kuvan esiin. makePic tulee noin kutsutuksi niin kellon tikityksillä kuin käyttäjän aiheuttamien tapahtumienkin yhteydessä, kunhan View on ensin käynnistetty start-metodilla. Kaikille View-olioille on määriteltävä makePic-metodi.

onTick-tulee kutsutuksi useita kertoja sekunnissa (ellei toisin määritellä), ja sitä myötä myös makePic.

(Vaikka sinun ei tarvitsekaan erikseen kutsua View’n tapahtumankäsittelijämetodeita ja makePiciä, niin ne ovat kyllä ihan tavallisia metodeita, joita voi kutsua tavalliseen tapaan. A+:n testikoodi ei varsinaisesti käynnistä laatimianne käyttöliittymiä ja esitä niitä ikkunassa; se kutsuu esim. onTickiä ja makePiciä suoraan.)

FlappyBug-tehtävä (osa 6/17: ajankulua)

Toteuta flappyView-oliolle onTick-metodi. Metodin tulee yksinkertaisesti kutsua näkymän kuvaaman peliolion timePasses-metodia. (Koska kyseessä on onTick, View-olio huolehtii automaattisesti siitä, että metodi tulee kutsutuksi jokaisella kellon naksahduksella.)

Kokeile.

Palauta.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

FlappyBug-tehtävä (osa 7/17: jotain rajaa)

Tehtävänanto

FlappyBug alkaa jo muistuttaa peliä mutta kaipaa monenlaista parannusta. Tee nyt nämä parannukset:

  • Estä ötökkää nousemasta pelialueen ylärajaa korkeammalle. Sen keskipisteen y-koordinaatti ei saa olla negatiivinen.

  • Estä ötökkää putoamasta maan pintaa matalammalle. Sen keskipisteen y-koordinaatti saa olla korkeintaan 350.

Suosittelemme seuraavaa kätevää ja eleganttia ratkaisutapaa. Muitakin toki on, kuten ohjelmoinnissa yleensäkin.

Vaihe 1: uusi apumetodi

Lisää Bug-luokkaan vaikutuksellinen metodi move:

  • Se ottaa parametrikseen Double-arvon, joka ilmaisee, paljonko ötökän y-koordinaattiin lisätään.

  • Se vaihtaa ötökän sijainniksi uuden koordinaattiparin, joka saadaan tekemällä vanhaan parametrin kokoinen lisäys. Positiivinen parametri siis siirtää ötökkää alas ja negatiivinen ylös.

Vaihe 2: refaktorointi

Toteuta aiemmin laatimasi metodit flap ja fall uusiksi niin, että kumpikin niistä kutsuu move-metodia ja antaa movelle tarkoitukseen sopivan parametrin. Metodien on siis tarkoitus saada aikaan ihan sama vaikutus kuin ennenkin, mutta ne toteutetaan nyt toisella tavoin. Kumpikin metodeista on toteutettavissa yksinkertaisesti move-metodia kutsumalla. (Älä tee mainittuihin metodeihin muita muutoksia. flap-metodin tulee edelleen vastaanottaa yksi parametri.)

Komeasti sanottuna se, mitä tässä teet, on refaktorointia (refactoring). Refaktorointi tarkoittaa ohjelman muokkaamista niin, että sen laatu paranee mutta toiminnallisuus ei muutu. Refaktoroimalla voi esimerkiksi parantaa ohjelman muokattavuutta.

Refaktoroidessa on viisasta testata, ettei mikään mennyt rikki (ns. regressiotestaus). Tässä meille riittäköön testaukseksi ohjelman koekäyttö. Kokeile ohjelmaasi; sen pitäisi toimia niin kuin ennen.

Vaihe 3: tutustu clampY-metodiin

Kaikilla Pos-olioilla on clampY-metodi. Sille annetaan parametreiksi kaksi lukua, jotka määräävät y-koordinaatin ala- ja ylärajan:

val testi = Pos(10, 50)testi: Pos = (10.0,50.0)
testi.clampY(5, 30)res4: Pos = (10.0,30.0)
testi.clampY(100, 200)res5: Pos = (10.0,100.0)
testi.clampY(0, 100)res6: Pos = (10.0,50.0)

clampY-palauttaa uuden sijainnin, jonka x-koordinaatti on sama kuin alkuperäinen mutta jonka y-koordinaatti on liiskattu halutulle välille. Tässä välinä oli 5–30, joten liian iso y-koordinaatti on vaihdettu ylärajaksi 30.

Sama toimii myös toisin päin, jos koordinaatti on alarajaa pienempi.

Jos koordinaatti jo on annetulla välillä, on tulos sama kuin alkuperäinenkin sijainti.

clampX

On olemassa myös vastaava clampX-metodi, jota et kuitenkaan tarvitse nyt.

testi.clampX(100, 200)res7: Pos = (100.0,50.0)

Vaihe 4: varsinainen ratkaisu

Noilla pohjustuksilla varsinainen ratkaisu on muutaman merkin mittainen.

Tarkoitus oli toisaalta estää y-koordinaattia kasvamasta liian isoksi, toisaalta liian pieneksi. Molemmat kärpäset liiskautuvat yhdellä kertaa, kun puristat ötökän sijainnin halutulle välille (0–350) clampYllä.

Tee tuo pieni lisäys move-metodiin.

Entä flap ja fall? Jos ja kun toteutit ne vaiheen 2 ehdottamalla tavalla, niin myös ne huomioivat nyt koordinaatin rajat, koska niiden toteutus perustuu move-metodiin. move-metodi hallinnoi nyt ötökän kaikkia liikkeitä.

Valinnainen vinkki

Pidä huoli, että ötökän uudeksi sijainniksi tulee sijoitetuksi luku, jossa on huomioitu sekä ötökän liike että (sen jälkeen) koordinaatin rajaus.

Metodikutsuja ketjuttaessasi muista, että Pos-oliot ovat muuttumattomia (luku 2.5). Kohdista myöhempi metodikutsu edellisen lopputulokseen.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

FlappyBug-tehtävä (osa 8/17: vauhdin hurmaa)

Ei tuollainen paikallaan hyppeleminen sovi ötökälle.

Voisimme pistää ötökän liikkumaan myös x-suunnassa. Mutta kokeillaan nyt toista tapaa tuoda liikkeen tuntua peliimme: pannaan tausta liikkumaan oikealta vasemmalle. Kätevänä apuna toimii eräs Pic-luokan metodi.

Pohjustus: shiftLeft

../_images/shiftLeft.png
val ympyra = circle(200, Red)ympyra: Pic = circle-shape
val siirretty = ympyra.shiftLeft(25)siirretty: Pic = circle-shape (transformed)
show(siirretty)

Oheisessa kuvassa näkyy shiftLeftin tuottama uusi kuva, jossa alkuperäistä on siirretty parametrin ilmoittama pikselimäärä vasemmalle. Samalla vasemmasta reunasta "kadonnut" osio on kiinnitetty oikeaan reunaan.

Aavistat jo varmaan idean: otetaan tuttu maisemakuva mutta siirretään sitä kellon tikittäessä vähitellen vasemmalle, jolloin saadaan aina uusi versio maisemasta taustakuvana käytettäväksi.

Tausta liikkeelle

Tee kolme muutosta FlappyBugin käyttöliittymään.

  1. Luo flappyView-oliolle uusi muuttuja nimeltään background. Alusta se sceneryn arvolla:

    var background = scenery
    

    Kyseessä on siis var. Käytämme tätä muuttujaa pitämään kirjaa siitä, mitä kuvaa parhaillaan käytetään taustakuvana. Aluksi taustana on se tuttu maisemakuva, jossa on puu keskellä.

    Muistutus: jotta backgroundista tulisi flappyView’n muuttuja, kirjaa se flappyView-olion määrittelevän koodin sisään. Älä kuitenkaan kirjaa sitä makePicin tai muunkaan metodin sisään — silloin siitä tulisi metodin paikallinen muuttuja, eikä se olisi olemassa kuin tuon metodin suorituksen aikana.

  2. Lisää onTick-metodiin tämä käsky, joka päivittää taustaa:

    this.background = this.background.shiftLeft(2)
    

    Taustaa siis siirretään pari pikseliä vasemmalle aina kellon lyödessä.

    Jos haluat, voit myös määritellä vakion ja käyttää sitä maagisen arvon 2 sijaan.

  3. Äskeiset käskyt kyllä pyörittelevät taustakuvaa muuttujassa, mutta jos kokeilet ohjelmaasi nyt, niin mitään havaittavaa muutosta ei näy. Metodi makePic nimittäin edelleen muodostaa näkymän käyttäen alkuperäistä scenery-kuvaa. Vaihda sieltä scenery backgroundiksi ja voilà!

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

FlappyBug-tehtävä (osa 9/17: ötökkä kiihtyy)

Ötökkämme putoaa hitaasti, tasavauhtia. Pannaan se liikkumaan siiveniskujen välillä kiihtyvää vauhtia kohti maata. Perusideana olkoon tämä:

  • Ötökällä on nopeus, joka ilmoittaa, montako pikseliä se liikkuu pystysuunnassa kullakin kellonlyömällä. Positiivinen nopeus tarkoittaa liikettä alaspäin ja negatiivinen ylöspäin.

  • Ötökän siiveniskut antavat sille vauhdin ylöspäin. Ötökkä ei siis heti siirry mihinkään vaan sen nopeus muuttuu (ks. tarkemmin alta).

  • Joka kellonlyömällä ötökän nopeuteen lisätään kaksi eli se tulee kiskotuksi aina vain kovempaa alaspäin.

Tarkemmat ohjeet: nopeusmuuttuja

Lisää Bug-luokkaan ilmentymämuuttuja, joka pitää kirjaa ötökän tämänhetkisestä nopeudesta pystysuunnassa: montako pikseliä ötökkä nousee tai putoaa kullakin viisariniskulla.

Aseta nopeusmuuttujan alkuarvoksi 0.0 ja nimeksi yVelocity.

Tarkemmat ohjeet: flap-metodi

Muuta flap-metodia niin, että se ei välittömästi siirräkään ötökkää ylöspäin tai muuallekaan. Sen sijaan siipien lyöminen antaa ötökälle vauhdin ylöspäin.

Nykyisessä ohjelmaversiossa flap-metodin parametri määräsi, paljonko ötökkää siirretään heti. Uudista metodia niin, että parametri sen sijaan määrääkin uuden ylöspäin suuntautuvan nopeuden. Esimerkiksi kun metodia kutsutaan parametriarvolla 15, ötökän nopeudeksi tulee -15. Sijoita tuo arvo nopeusmuuttujaan yVelocity.

Uudessa ohjelmaversiossa flap-metodin kutsuminen ei siis saa muuttaa ötökän sijaintia lainkaan! Tämä metodi muokkaa vain nopeutta.

Huomaa, ettei flapin toiminta lainkaan riipu siitä, mikä ötökän aiempi nopeus oli. Tämä metodi vaihtaa vanhan nopeuden kokonaan uuteen, ei vähennä lukua vanhasta nopeudesta.

(Game-luokan activateBug-metodista pitää (edelleen) kutsua Bug-luokan flap-metodia juuri tuolla parametriarvolla 15 kuten ennenkin. Mutta se, mitä flap-metodi tuolla parametriluvullaan tekee, muuttuu.)

Tarkemmat ohjeet: fall-metodi

Muuta fall-metodia niin, että se

  • ensin kasvattaa yVelocity-muuttujan arvoa: uudeksi arvoksi tulee vanha arvo plus kaksi

  • ja sitten siirtää ötökkää yVelocity-muuttujan uuden arvon verran (ylös tai alas; riippuu nykyisestä arvosta). Koska toteutit luokkaan move-apumetodin aiemmassa tehtävässä, niin tämä hoituu sitä kutsumalla.

Sivuhuomio move-metodista

Nyt sattui käymään niin, ettei move-apumetodimme ole tässä pelin versiossa enää aivan yhtä hyödyllinen kuin aiemmassa. Sellaista sattuu ohjelmoidessa. Mutta kyllä tuon metodin voi edelleen hyvin jättää koodiin ja hyödyntää sitä. Jätä se sinne.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Jatkamme FlappyBugin kanssa luvussa 3.2.

Ohjelmani huomaa hiiren (ym.)

Seuraavissa vapaaehtoisissa tehtävissä muun muassa reagoidaan hiiren liikkeisiin, mikä ei ole kurssin pisteytettyjen tehtävien kannalta välttämätöntä mutta mitä varmaan haluat silti kokeilla.

Lisätehtävä: jotain kiinni hiiressä

../_images/mouse_burr_DwightKuhn.jpg

kuva: Dwight Kuhn

Tässä leluesimerkissä mallinnamme takiaista:

class Takiainen:
  var sijainti = Pos(0, 0)   // tuoreimman säilyttäjä

Takiaisolion ainoa ominaisuus on siis sen sijainti, jota voi muuttaa:

val testi = Takiainen()testi: Takiainen = o1.takiainen.Takiainen@34ece05e
testi.sijaintires8: Pos = (0.0,0.0)
testi.sijainti = Pos(10, 50)testi.sijaintires9: Pos = (10.0,50.0)

Pikkusovelluksia-moduulista löytyy paitsi tuo takiaisluokka myös tiedosto Takiaisohjelma1.scala. Täydennä koodinraakile tällaiseksi sovellukseksi:

  • View-ikkunan otsikko on "Takiaisohjelma".

  • Sovelluksen käyttöliittymänä on näkymä, joka piirtää vihertävän ympyrän (takiaisenKuva) annettua valkoista taustakuvaa (tausta) vasten. Ympyrä piirretään takiaisen koordinaatteihin.

    • makePic-metodin on palautettava sellainen kuva, jossa takiaisen kuva on oikeassa kohdassa taustakuvan päällä.

    • Anna tässä ohjelmassa View-luokasta johtamallesi oliolle nimeksi nakyma. (Tämä ei ole muuten tärkeää mutta helpottaa automaattista arviointia.)

  • Näkymällä on tällainen tapahtumankäsittelijämetodi:

    • Metodin nimi on onMouseMove ja parametrina yksi hiiren sijainnin kertova Pos-olio (aivan kuin onClick-metodilla ylempänä).

    • Metodi sijoittaa takiaiselle uudeksi sijainniksi hiiren kursorin koordinaatit. Takiainen siis seuraa hiirtä.

    • Voit lisäksi lisätä metodin runkoon käskyn, joka tulostaa parametriarvon. Siten näet ohjelmaa kokeillessasi, miten usein tuo metodisi tulee kutsuttua, kun siirtelet hiirtä ikkunan päällä.

    Huomaa, ettei onMouseMove-tapahtumankäsittelijämetodi tee kuvilla mitään. On makePic-metodin tehtävä muodostaa kuva käyttäen apuna sitä viimeisintä koordinaattiparia, jonka onMouseMove on takiaiselle sijoittanut.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Jatko edelliselle: takiaisesta tähtäimeksi

Piirretään ympyrän sijaan kaksi viivaa, jotka leikkaavat liikkuvan kursorin kohdalla.

../_images/line.png

Viivoja on helppo luoda o1-pakkauksen line-funktiolla, joka toimii samaan tapaan kuin circle, rectangle ja vastaavat. Tässä pieni esimerkki:

val viiva = line(Pos(0, 0), Pos(150, 100), Red)viiva: Pic = line-shape
val tausta = circle(200, LightBlue)tausta: Pic = circle-shape
val kuva = tausta.place(viiva, Pos(20, 20))kuva: Pic = combined pic
../_images/tahtain.png

Suunnilleen tältä pitäisi näyttää. Viivat liikkuvat kursorin mukana.

Ota esiin o1.takiainen.viivat-pakkaus ja sieltä Takiaisohjelma2.scala. Kopioi sinne pohjaksi edelliseen tehtävään tekemäsi ratkaisu. Muuta makePic-metodia siten, että se ei piirräkään "takiaisen kuvaa" vaan asemoi taustaa vasten kaksi mustaa (Black) viivaa:

  • Yksi viivoista alkaa yläreunasta suoraan kursorin sijainnin yläpuolelta ja piirtyy pystysuoraan aina alareunaan saakka.

  • Toinen viiva alkaa vasemmasta reunasta suoraan kursorin sijainnin vasemmalta puolelta ja piirtyy oikeaan reunaan saakka.

Ohjeita ja vinkkejä:

  • Voit käyttää place-metodia asemoidaksesi viivan taustaa vasten. Kun teet niin, huomaa, että viiva kiinnittyy alkupäästään eikä keskeltä. Anna siis placen koordinaattiparametriksi Pos-olio, joka kertoo, mistä haluat viivan alkavan.

  • onMouseMove-metodia ei ole syytä muuttaa.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Jatkotehtävä: Pos-laskentaa

Laadi pakkauksen o1.takiainen.osoitin tiedostoon Takiaisohjelma3.scala edellisiä muistuttava sovellus, jossa näkyy takiaispalluran ja ristiviivojen sijaan yksi ainoa musta viiva, joka piirtyy aina näkymän keskikohdasta hiiren kursoria kohti mutta vain puolet matkasta. Viiva siis ikään kuin "osoittaa" keskeltä näkymää kursoria kohti kuitenkaan yltämättä kursoriin.

Tehtävä vaatii hieman koordinaateilla laskemista ja ratkeaa kätevämmin, jos käytät apuna Pos-luokan metodeita add, multiply ja/tai divide:

  • Kokeile kutsua add-metodia antaen parametriksi viittauksen toiseen Pos-olioon: sijainti1.add(sijainti2).

  • Voit myös kertoa tai jakaa molemmat koordinaatit luvulla: sijainti1.multiply(luku) tai sijainti1.divide(luku).

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Jatkotehtävä: tikittävä takiainen

Laadi pakkauksen o1.takiainen.hidas tiedostoon Takiaisohjelma4.scala sovellus, jossa takiaista kuvaava pallura seuraa kursoria kuten ensimmäisessäkin näistä ohjelmista. Kuitenkaan tällä kertaa pallura ei suit sait ilmesty sinne, missä kursori on, vaan lipuu kursoria kohti vähitellen.

Kopioi pohjaksi ensimmäiseen takiaisohjelmaan kirjoittamasi koodi ja muuta sitä seuraavasti:

  • Lisää nakyma-oliolle muuttuja, jossa pidät kirjaa tuoreimmasta havaitusta hiirikursorin sijainnista. Aluksi se voi osoittaa esimerkiksi koordinaatteihin (0,0). Muuttujan nimi voi olla esimerkiksi viimeisinKursori.

  • Muokkaa onMouseMove-metodia siten, että se ei tee muuta kuin sijoittaa vastaanottamansa parametriarvon (eli kursorin senhetkisen sijainnin) viimeisinKursori-muuttujan arvoksi. Tässä ohjelmassa hiiren liikuttaminen ei siis suoraan liikuta takiaista vaan vain pistää talteen sen, mihin hiiri viimeksi liikkui.

  • Lisää onTick-metodi, joka siirtää takiaista. Uusi sijainti saadaan laskemalla piste, joka on 10 % matkasta takiaisen aiemmasta sijainnista kohti viimeisintä kursorin sijaintia.

    • Esimerkiksi jos takiainen on ennestään sijainnissa (10,20) ja viimeisinKursori-muuttujassa on sijainti (100,100), niin takiaisen uudeksi sijainniksi tulee (19,28).

    • Tähän laskutoimitukseen voit hyödyntää askeisessä tehtävässä mainittuja Pos-luokan metodeita.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lisätehtävä: piirto-ohjelma

Tutustu seuraavaan pseudokoodiin luokasta, joka kuvaa "taideprojekteja" eli kuvia, joihin vähä vähältä lisätään väripisteitä. On tässä pseudokoodissa valmistakin Scalaa jo mukana:

class Taideprojekti(tausta: Pic):

  var kuva = tausta                  // kokooja
  var pensseli = circle(10, Black)   // tuoreimman säilyttäjä

  def piirra(mihin: Pos) =
    Sijoita kuva-muuttujalle uusi arvo, joka saadaan asemoimalla sen aiemman
    arvon päälle pensselin kuva mihin-parametrin osoittamaan kohtaan.

end Taideprojekti

kuva-muuttuja on rooliltaan kokooja: aluksi sen arvona on vain taustakuva mutta vähitellen taustan päälle lisätään pieniä kuvia, "pensselinjälkiä". Muuttuja siis viittaa kuvaan, joka on taustakuvan ja kaikkien lisättyjen pensselijälkien yhdistelmä. (Näin on ainakin tarkoitus, mutta pensselinjälkiä lisäävä metodi on vielä toteuttamatta.)

"Pensseli" on kuva, josta piirtyy uusi kopio yhdistelmäkuvan päälle aina, kun piirra-metodia kutsutaan. Oletusarvoisesti pensselinä on pieni musta ympyrän kuva.

Algoritmi on annettu mutta jätetty sinun toteutettavaksesi Pikkusovelluksia-moduulin pakkaukseen o1.taide.

Samasta paikasta löytyy myös Piirustusohjelma.scala. Tutustu siihen; huomaat, että se luo näkymän, jonka mallina toimii Taideprojekti-olio. Annettu koodi on hyvä alku, mutta tapahtumankäsittelijä puuttuu. Joten:

  1. Toteuta Taideprojekti-luokan piirra-metodille runko.

  2. Lisää käyttöliittymään tapahtumankäsittelijä onMouseMove, joka yksinkertaisesti kutsuu näkymässä näkyvälle teokselle piirra-metodia lisätäkseen (place) pensselinjäljen hiiren kursorin kohdalle.

  3. Kokeile.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Hieman haastavampi jatko: värejä piirto-ohjelmaan

Muokkaa äskeistä piirto-ohjelmaa niin, että hiiren klikkaus vaihtaa piirtoväriä.

Muokkaa ensin Taideprojekti-luokkaa niin, että se pitää kirjaa valitusta piirtoväristä ja vaihtaa pyydettäessä seuraavaan. Tee siis seuraavat muutokset:

  1. Lisää alkuun kaksi ilmentymämuuttujaa:

    var varinumero = 0
    val paletti = Buffer(Black, Red, Green, Blue)
    

    Muuttuja paletti osoittaa kokoelmaan värejä, joiden välillä käyttäjä voi vaihdella.

    Yksi paletin väreistä on kerrallaan aktiivisena; muuttuja varinumero kertoo, monesko. Tässä tehtävässä tuota muuttujaa on tarkoitus käyttää askeltajana, joka käy läpi paletin indeksejä järjestyksessä palaten aina lopusta alkuun: 0, 1, 2, 3, 0, 1, 2, 3, 0, 1 jne.

  2. Toteuta luokkaan parametriton ja vaikutukseton metodi piirtovari, joka palauttaa parhaillaan valittuna olevan piirtovärin. Siis sen paletin väreistä, johon varinumero osoittaa (aluksi musta).

  3. Toteuta vaikutuksellinen metodi vaihdaVaria, joka toimii näin:

    • Se vaihtaa varinumero-muuttujan arvoksi seuraavan luvun. (Käytä nollaan palaamiseen modulo-operaattoria.)

    • Se vaihtaa pensseli-muuttujan arvoksi uuden samankokoisen mutta erivärisen ympyrän. Käytä piirtovari-metodia apuna.

    • Se on parametriton (mutta määritellään tyhjillä kaarisulkeilla, koska on vaikutuksellinen; luku 2.6).

Lisää sitten käyttöliittymään tapahtumankäsittelijä onClick, joka (Pos-tyyppisen parametrinsa arvosta välittämättä) kutsuu teoksen vaihdaVaria-metodia. Voit ottaa mallia tämän luvun alun klikkausohjelmasta.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Jatkoa edelliselle: lisää käyttöliittymätapahtumista

Muokkaa äskeisessä tehtävässä laatimaasi ohjelmaa niin, ettei väriä vaihdetakaan aina seuraavaan, kun käyttäjä klikkaa. Sen sijaan käyttäjän peräkkäisten klikkausten lukumäärä määrää uuden piirtovärin: yksi klikkaus valitsee ensimmäisen värin, tuplaklikkaus toisen, kolmoisklikkaus kolmannen ja niin edelleen.

Muokkaa ensin Taideprojekti-luokkaa. Lisää vaihdaVaria-metodille Int-parametri. Muuta metodia niin, että se ei askellakaan seuraavaan väriin vaan asettaa varinumeron saamansa parametriarvon perusteella.

Korvaa sitten piirustusikkunan tapahtumankäsittelijä onClick tällä uudella versiolla:

override def onClick(klikkaustapahtuma: MouseClicked) =
  teos.vaihdaVaria(klikkaustapahtuma.clicks)

Aiemmassa versiossa parametriksi saatiin vain klikkauksen sijainti (Pos). Sellainen riittää moneen tarkoitukseen. Jos kuitenkin haluamme tarkempia tietoja tapahtumasta, kuten vaikkapa peräkkäisten klikkausten lukumäärän, voimme...

... määritellä tapahtumankäsittelijän, joka saa parametrikseen viittauksen kaikkia tapahtuman tietoja kuvaavaan olioon. Hiirenklikkaustapahtumia kuvaa luokka MouseClicked.

MouseClicked-oliolta voi kysellä monenlaisia tietoja. Tässä meille riittää clicks, joka on napsautusten lukumäärän kertova kokonaisluku.

Testaa ohjelmaasi. Kokeile myös mitä tapahtuu, jos klikkaat lukuisia kertoja peräkkäin. Huomioitko tämän mahdollisuuden vaihdaVaria-metodissa? Jos et, huomioi se jotenkin; jos kyllä, voit silti kokeilla mitä tapahtuu, jos tuon jättää huomioimatta.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Halutessasi voit jatkaa tapahtumankuuntelijoiden ja käyttöliittymätapahtumien tutkimista. Esimerkiksi View-luokan dokumentaatiosta löydät luettelon käytettävissä olevista on-alkuisista tapahtumankäsittelijämetodeista.

Yhteenvetoa

  • Kun käyttäjä vuorovaikuttaa graafisen käyttöliittymän kanssa, syntyy ns. GUI-tapahtumia. Tapahtuma voi olla esimerkiksi näppäimen painallus tai hiiren liikahdus. Tapahtumiksi voidaan lukea myös sovelluksen sisäisen kellon “lyönnit”.

  • Tapahtumankäsittelijäksi sanotaan aliohjelmaa, jolle tieto tapahtumasta välitetään ja joka määrittää, mitä ohjelma tällöin tekee.

    • Tapahtumankäsittelijä voi saada parametrina lisätietoa tapahtumasta, kuten hiirenpainalluksen sijannin.

    • Tällä kurssilla käyttämämme View-luokan olioille voi kirjoittaa tapahtumankäsittelijöitä.

  • Graafisten ohjelmien tekeminen on aika kivaa.

  • Lukuun liittyviä termejä sanastosivulla: malli, käyttöliittymä; graafinen käyttöliittymä eli GUI; käyttöliittymätapahtuma, tapahtumankäsittelijä, tapahtumankuuntelija; refaktoroida; askeltaja.

Saa luoda!

Tämän luvun välineillä voi tehdä paljon muutakin kuin mitä yllä ehdotettiin. Sen kun kokeilet! Muokkaa jotakin esitellyistä ohjelmista tai keksi jotain ihan omaa.

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.

Lisäkiitokset tähän lukuun

FlappyBug on saanut vaikutteita Dong Nguyenin pelistä.

Lisätehtävä, jossa piirretään viiva keskeltä kohti kursoria, on Scala-kielinen muunnelma Daniel Shiffmanin käyttämästä ohjelmointiharjoituksesta.

a drop of ink
Palautusta lähetetään...