tiistai 26. toukokuuta 2015

7. luokan matematiikan tunneilla koodattiin Racket - pelejä

Syksyllä kun aloitimme Racket - koodauksen opiskelun, menin lupaamaan oppilaille, että koodaisimme vielä pelin. Keväällä viimeisen matikan kokeen jälkeen oli tullut aika lunastaa lupaukset...

Käytimme pelin koodaamiseen tekemääni Peruspelin pohjaa, eli hyvin pitkälle vietyä koodia, johon oppilaiden piti täydentää vain muutamia rivejä Racket - koodia. Päätin käyttää pelin tekemiseen Bootstrap - projektin WeScheme - sivustoa. Se mahdollistaa pelin koodaamisen selaimen kautta. Koska WeScheme toimii Google - tilin kautta, tein ryhmälle yhden yhteisen Google - tilin ja kopioin valmiiksi kahdeksan Peruspeli - pohjaa. Tähän päädyin, koska halusin, että peliä tehtäisiin pareittain useamman tunnin aikana (ja molempien oppilaiden piti päästä koodiin käsiksi).

Huom! Jos haluat kokeilla peruspelipohjaa, kirjaudu ensin WeScheme-sivustolle ja avaa sitten peruspelin linkki ja tallenna.
WeScheme.org sivustolle kirjaudutaan Google-tunnuksilla
Ensimmäiset kaksi tuntia meni pelin suunnitteluun ja grafiikoiden työstämiseen. Pelin idea on hyvin yksinkertainen: pelaaja liikkuu näppäimillä ylös ja alas, ja samalla väistelee vaaroja ja kerää kerättäviä, jotka liikkuvat vaakasuoraan. Oppilaat etsivät netistä taustakuvat sekä pelaajan, vaaran ja kerättävän kuvan. Tässä yhteydessä kävimme läpi myös sen, miten Googlen kuvahausta filteröidään näkyville vain sellaiset kuvat, joita saa uudelleenkäyttää ja muokata (kuinkahan moni lopulta näin teki?).

Pelaajan, kerättävän ja vaaran kuvista piti poistaa taustat (tehdä tausta läpinäkyväksi). Tähän käytimme GIMP2:sta (joku toinen ohjelma olisi voinut olla kätevämpi). Kuvat (png) ladattiin jokaisen omaan peliin WeSchemessä (kuvat tallentuvat samalla GoogleDriveen). Kuvan lataaminen generoi valmiin linkin koodin (HUOM! kursorin piti olla oikeassa kohdassa koodia, ettei generoinut linkkiä väärään kohtaan):

(bitmap/url "https://drive.google.com/uc?export=download&id=0B5CH7_F_YAvoX2RrbDl3N3JGam8")

Kuvat ladataan WeSchemeen Images-napin kautta
Kuvien lataamisen jälkeen niitä piti tietysti skaalata (scale) oikean kokoisiksi. Vaara ja kerättävä myös siirrettiin oikeille lähtöpaikoilleen, eli pelikentän reunoille. Tämä tehtiin muuttamalla kahden vakion arvioa:

(define VAARA-LÄHTÖ-X -50)
(define KERÄTTÄVÄ-LÄHTÖ-X 750)

Kun tämän jälkeen pelihahmot piti saada liikkeelle. Alussa "liikuta-varaa" - funktio näytti tältä:

;(check-expect (liikuta-vaaraa ..) ..)
;(check-expect (liikuta-vaaraa ..) ..)

;; liikuta-vaaraa : Number -> Number
(define (liikuta-vaaraa x)
  x)


Mietimme ensin mihin suuntaan vaaran piti liikkua ja teimme pari esimerkkiä. Jos vaara on kohdassa x=100 niin funktiokutsun jälkeen se on kohdassa x=110 (eli liikkuu oikealle). Tämä kirjoitettiin check-expect - lauseeseen:

(check-expect (liikuta-vaaraa 100) 110)

Kun funktion toiminta oli ymmärretty, täydennettiin funktion koodi toimivaksi:
;; liikuta-vaaraa : Number -> Number
(define (liikuta-vaaraa x)
  (+ x 10))


Vastaava operaatio tehtiin kerättävälle. Tietysti mielessä piti pitää mihin suuntaan halusi hahmojen etenevän (mahdollisesti tarvittiin miinusmerkkiäkin).

Nyt hahmot jatkoivat eteenpäin tulematta koskaan takaisin joten lisäsimme testin, jolla voitiin testata ollaanko vielä sallitulla alueella. Aloitimme chack-expect - pohdinnoilla, ja sitten kirjoitimme funktion.

(check-expect (alueella? -100) false)
(check-expect (alueella? 100) true)
(check-expect (alueella? 1000) false)

;; alueella? : Number -> Boolean
(define (alueella? x)
  (<= -50 x 750))
 

Tässä piti tajuta ottaa mukaan välin päätepisteet (jos käytti samoja lukemia kuin VAARA-LÄHTÖ-X ja KERÄTTÄVÄ-LÄHTÖ-X) muuten hahmo jäi nykimään eikä lähtenyt enää liikkeelle ollenkaan.

Seuraava vaihe oli näppäinohjauksen lisääminen pelaajalle. Suunnittelimme senkin ensin yhdessä check-expect lauseiden avulla. Nuolinäppäimet ovat "up" ja "down", tässä voi käyttää myös "w" ja "s" näppäimiä:

(check-expect (siirrä-pelaajaa 100 "up") 120)
(check-expect (siirrä-pelaajaa 100 "down") 80)
(check-expect (siirrä-pelaajaa 100 " ") 100)

;; siirrä-pelaajaa : Number Key -> Number
(define (siirrä-pelaajaa y näppäin)
  (cond [(key=? näppäin "up")(+ y 20)]

        [(key=? näppäin "down")(- y 20)]
        [else y]))

Nyt hahmot liikkuivat mutta mitään ei tapahtunut vaikka ne törmäsivät. Peruspelissä hahmojen törmääminen päätellään siitä, ovatko ne pelaajan ympärille piirretyn laatikon sisällä. Säädimme ensin laatikon koon sopivaksi ja katselimme sitä "pause"-moodissa (painetaan pelissä p-näppäintä).

(define TÖRMÄYSVARA-X 50)
(define TÖRMÄYSVARA-Y 80)


Pause - moodissa nähdään hahmojen sijainnit sekä laatikon koko (keltainen)
Koko pelin vaikein lauseke oli törmäysehtojen kirjoittaminen. Aloitimme x-suunnasta. Jotta törmäys tapahtuisi, täytyisi hahmon x-koodinaatin (x2) olla laatikon koordinaattien sisällä. Vasemman reunan x-koodinaatti saataisiin laskemalla: x1 - TÖRMÄYSVARA-X ja oikea laita laskemalla x1 + TÖRMÄYSVARA-X. Matemaattisesti ilmaistuna siis:

(x1 - TÖRMÄYSVARA-X) <  x2  < (x1 + TÖRMÄYSVARA-X)


Törmäysvarat
Ensimmäinen ehto Racket - muotoon kirjoitettuna olisi:

(define (törmäsikö? x1 y1 x2 y2)
  
(< (- x1 TÖRMÄYSVARA-X) 
       x2 
      (+ x1 TÖRMÄYSVARA-X))

Tätä testattaessa käy nopeasti ilmi, että törmäys tapahtuu aina kun pelaaja ja vaara/kerättävä ovat samalla linjalla, joten tarkistus on tehtävä myös y-koordinaatille. Tähän tarvitaan myös and:iä. Valmis funktio oli tällainen:

(define (törmäsikö? x1 y1 x2 y2)
  
(and (< (- x1 TÖRMÄYSVARA-X) 
         x2 
        (+ x1 TÖRMÄYSVARA-X))
        (< (- y1 TÖRMÄYSVARA-Y) 
         y2 
        (+ y1 TÖRMÄYSVARA-Y))))

Viimeinen silaus pelille saadaan, kun lisätään pisteiden päivittäminen. Pelissä on kaksi tilannetta "törmäys-vaara" ja "törmäys-kerättävä". Tämänkin funktion suunnittelu tehtiin check-expect:ien kanssa:

(check-expect (päivitä-pisteet 100 "törmäys-vaara") 90)
(check-expect (päivitä-pisteet 100 "törmäys-kerättävä") 110)

;; päivitä-pisteet : Number String -> Number
(define (päivitä-pisteet pisteet tapahtuma)
  (cond [(string=? tapahtuma "törmäys-vaara") (- pisteet 10)]

        [(string=? tapahtuma "törmäys-vaara") (+ pisteet 10)] ))        
  
Pelien tekemiseen meni kaikkiaan 6 matematiikan oppituntia (niistä kaksi kuvien käsittelyyn). Aivan kaikki parit eivät saaneet peliänsä valmiiksi mutta valmiitakin pelejä saatiin aikaiseksi. Yksi pari jatkoi koodauskerhossa aloitettua peliänsä (Bootstrap II pelipohjalla tehty). Pelin tekeminen tuntui kiinnostavan oppilaita, ja oli tärkeää että jokaisella oli oma ideansa toteutettavaksi. Peliä tehdessään oppilaat kyselivät mm. miten kerättävät saisi liikkumaan pystysuuntaan tai pelaajan sivusuuntaan, miten voisi vaihtaa pelaajan hahmoa, zoomata lähemmäs tms. Peruspelin pohjalla ei näitä muutoksia kuitenkaan tässä ajassa olisi pystynyt tekemään, vaikka kaikki on tietysti periaatteessa mahdollista... Aikaa oli nytkin hieman liian vähän, asioiden sulatteluun menee  yllättävän kauan. Pari tuntia olisi tarvittu vielä lisää niin olisimme saaneet kaikki pelit valmiiksi, ja ehtineet hieman pelatakin niitä.

Tällaisia esimerkkejä siitä millaisia pelejä matikan tunnilla syntyi (osasta puuttuu pisteiden laskeminen). Pelaaminen joko nuolinäppäimillä tai s ja w näppäimillä. Peli alkaa välilyönnillä (myös uusi peli alkaa välilyönnillä). Voit katsoa törmäysaluetta pause - moodilla (paina "p"):

Ammu tuomari - peli (pelaa)
Racket War:issa kerätään Sprite - juomia ja vältellään punaista rakettia (pelaa)
Apinapelissä kerätään banaaneja ja vältellään leijonaa (pelaa)
Homer-pelissä pisteitä saa, jos hamppari osuu Homerin suuhun, pisteitä lähtee jos se joutuu roskikseen (pelaa)
Avaruuspelissä ammutaan vihreitä ammuksia ja pujotellaan asteroidien välistä (pelaa)
Tämä ryhmä oli koodanneet Racket - kielellä jo aikaisemmin, pohjalla oli n. 10 tuntia Racket-koodausta. Osalle oppilaista oli selvästikin syntynyt jonkinlaista ymmärrystä ohjelmoinnista, joten pelin tekeminen ei tuntunut ollenkaan mahdottomalta. Mielenkiinnolla odotan mitä tämä porukka saa aikaan syksyllä kun peliohjelmoinnin kurssi alkaa. 

Ei kommentteja:

Lähetä kommentti