!!7. Graafiliste mängude loomine
Mängude loomine on [[https://en.wikipedia.org/wiki/Video_game_industry | väga suur]] valdkond, kus programmeerijad saavad oma võimeid kasutada. Mänge on muidugi võimalik ka Pythoniga luua. Selle jaoks on loodud erinevaid teeke: [[http://www.pygame.org/ | Pygame]], [[http://pyglet.org/ | pyglet]], [[http://cocos2d.org/ | Cocos2d]], [[https://www.panda3d.org/ | Panda3D]] ja [[https://wiki.python.org/moin/PythonGameLibraries | palju muid]]. Siin peatükis vaatame, kuidas luua üks lihtne mäng Pygame'iga. 

Pygame on populaarne Pythoni mängude loomise teek, mis lubab lihtsasti tekitada graafikat ja heli ning saada kasutajalt sisendit mitmel erineval moel. See on hea viis, kuidas alustada oma mänguarendaja karjääri. See pole ka ainult alustamiseks: [[https://itch.io/games/made-with-pygame | päris palju]] mänge on Pygame'iga tehtud. 

!!!Ettevalmistus
Enne jätkamist tuleb paigaldada moodul [[https://www.pygame.org/wiki/GettingStarted#Pygame%20Installation | pygame]]. Seda saab teha käsuga @@pip install pygame@@. Katsetamiseks kirjuta Pythonis @@import pygame@@. Kui erindit ei visata, on moodul paigaldatud. Kui ei ole kindel, kuidas mooduleid installida, siis on õpikus [[https://progeopik.cs.ut.ee/osa1.html | moodulite paigaldamise juhised]]. 

Et peatükist aru saada, peab olema läbitud õpiku [[https://progeopik.cs.ut.ee/osa1.html | esimene osa]]. Soovitatav on läbida peatükk "Objektorienteeritud programmeerimine" läbimine, aga see ei ole vajalik. 

!!!Mängu akna kuvamine
Kui moodul @@pygame@@ importida, siis saab Pygame'i käivitada käsuga @@pygame.init()@@. Enne seda peab veel määrama akna suuruse käsuga @@pygame.display.set_mode()@@, mille parameetriks on ennik ekraani laiusega ja kõrgusega pikslites. Pygame'i akent saab sulgeda käsuga @@pygame.quit()@@. Teeme akna laiusega 640 pikslit ja kõrgusega 480 pikslit.

(:codestart python gutter='false':)
import pygame

pygame.display.set_mode((640, 480))
pygame.init()

pygame.quit()
(:codeend:)

Kui see programm käivitada, siis ilmub korraks must aken suurusega 640 korda 480 pikslit ja läheb kinni.

Pygame'i loodud aken töötab eraldiseisvalt Pythoni programmile. See tähendab, et pärast @@pygame.init()@@ käsku kuvatakse eraldi aken, kuhu saab hakata joonistama ning programmi kood jookseb edasi. Aken läheb kinni alles siis, kui programm jõuab sulgemiskäsuni @@pygame.quit()@@. Kui enne sulgemiskäsku jookseks programm 3 sekundit, siis aken on püsti 3 sekundit. 

Kui enne sulgemiskäsku jooksutada lõpmatu tsükkel, siis aken ei lähegi kinni ning ristist sulgemine ka ei aita. Programmi töö peab muudmoodi lõpetama. Saame selles lõputus while-tsüklis kontrollida, kas kasutaja on vajutanud sulgemisnupule ja sel juhul saame tsükli lõpetada.

(:codestart python gutter='false':)
import pygame

pygame.display.set_mode((640, 480))
pygame.init()

while True:
    sisend = pygame.event.poll()
    if sisend.type == pygame.QUIT:
        break

pygame.quit()
(:codeend:)

Attach:silmaring7_1.png


Kui see programm käivitada ja ristile vajutada, siis aken läheb kinni. Oleme kirjutanud väga algelise programmi, millega kasutaja saab suhelda! Vaatame hiljem, kuidas tsüklisse muud funktsionaalsust lisada ja teistmoodi kasutajalt sisendit saada. Kõigepealt proovime aknasse midagi joonistada.

!!!Joonistamine
Siiamaani oleme loonud musta akna, mis on graafilisest mängust tsipa kaugel. Järgmisena proovime selle akna millegi värvilisega täita. 

!!!!Värvid
Saame täita selle akna mingi värviga, mida saab esindada erinevatel viisidel, aga Pygame'is on tavaks kasutada [[https://et.wikipedia.org/wiki/RGB | RGB mudelit]], mis koosneb punasest, rohelisest ja sinisest värvi kogusest, 0 kuni 255. Neid esindatakse kolmeliikmeliste ennikutena. Näiteks on must värv (0, 0, 0), valge värv (255, 255, 255), punane värv (255, 0, 0), akvamariinsinine (128, 255, 212) jne. Lihtne viis enda värvi valimiseks on [[https://www.google.ee/search?q=colour+picker | guugeldada "colour picker"]] ja võtta valitud värvi RGB väärtused.

Täidame akna enda soovitud värviga. Selleks peame ekraanisuuruse muutmise funktsiooni tagastuse salvestama muutujasse @@ekraan@@. See on muutuja, kuhu hakkame edaspidi kõiki joonistusi lisama. Ekraani saab täita meetodiga @@fill()@@, mille parameetriks läheb värv. Tehtud muudatused tuleb salvestada käsuga @@pygame.display.flip()@@.

(:codestart python gutter='false':)
import pygame

ekraan = pygame.display.set_mode((640, 480))
pygame.init()

ekraan.fill((128, 255, 212))
pygame.display.flip()

while True:
    sisend = pygame.event.poll()
    if sisend.type == pygame.QUIT:
        break

pygame.quit()
(:codeend:)

Programm käivitab nüüd akna, mis pole enam must, vaid akvamariinsinine. Edusammud!

Attach:silmaring7_2.png

!!!!Kujundid
Proovime lõpuks aknasse midagi lisada. Pygame võimaldab joonistada erinevaid kujundeid. Ristkülikut saab ekraanile joonistada funktsiooniga @@pygame.draw.rect()@@. See nõuab kolme parameetrit:

# pind, mille peale joonistada (meie puhul muutuja @@ekraan@@)
# värv, millega ristkülik täita (värve juba oskame)
# asukoha nelik: alguspunkti x-koordinaat, alguspunkti y-koordinaat, ristküliku laius, ristküliku kõrgus (kõik pikslites)

Koordinaatide lugemist alustatakse Pygame'is ülevalt vasakult ehk (0, 0) on üleval vasakul, (640, 0) on üleval paremal, (0, 480) on all vasakul ja (640, 480) on all paremal. Kõik vahepealsed on kuskil ekraani sees. 

Joonistame roosa ristküliku, mis algab meie akna keskpunktist ehk (320, 240) ning on 200 piksli laiune ja 100 piksli kõrgune. 

(:codestart python gutter='false':)
import pygame

ekraan = pygame.display.set_mode((640, 480))
pygame.init()

ekraan.fill((128, 255, 212))

pygame.draw.rect(ekraan, (255, 128, 171), (320, 240, 200, 100))
pygame.display.flip()

while True:
    sisend = pygame.event.poll()
    if sisend.type == pygame.QUIT:
        break

pygame.quit()
(:codeend:)

Attach:silmaring7_3.png


Pygame lubab ka teisi kujundeid joonistada. Loe [[https://www.pygame.org/docs/ref/draw.html | dokumentatsioonist]] erinevaid võimalusi ja proovi ekraanile joonistada ring, kolmnurk ja joon. 

!!!!Pildid
Sarnaselt kujunditele saab mängu lisada pilte. Kõigepealt peame leidma pildi, mida mängus kasutada. Neid saab muidugi ise luua, aga Internetis on suur hulk vabalt kasutatavaid materjale olemas: [[https://itch.io/game-assets/free/tag-2d | itch.io/game-assets]], [[https://opengameart.org/collections | opengameart.org]] [[https://www.google.com/search?q=free+game+sprites | jpm]]. Kui neid kasutada mängudes, on vaja autorile viidata. Siin peatükis kasutame rotti, mis on võetud [[https://opengameart.org/content/rodents-rat-rework | siit]], ja juustu, mis on võetud [[https://opengameart.org/content/breakfast-pixel-art | siit]]. 

Attach:rott.png
Attach:juust.png

Et joonistada pilt aknasse, tuleb see kõigepealt sisse laadida pildiobjektina funktsiooniga @@pygame.image.load()@@. Sulgudesse läheb failitee. Praegu paigutame pildifailid programmifailiga samasse kausta, aga suurema mängu puhul võiks need olla eraldi kaustas.

(:codestart python gutter='false':)
rott = pygame.image.load("rott.png")
(:codeend:)

Muutuja rott on nüüd pind ([[https://www.pygame.org/docs/ref/surface.html | Surface]]), mida saab joonistada ekraanile. Pindade võimaluste kohta saab lugeda [[https://www.pygame.org/docs/ref/surface.html | Pygame'i dokumentatsioonist]]. 

Saame roti paigutada ekraanile meetodiga @@blit()@@. Sulgudesse läheb pildifail ning koordinaadipaariga asukoht. Paigutame loodud roti objekti ekraani koordinaatidele (100, 200).

(:codestart python gutter='false':)
ekraan.blit(rott, (100, 200))
(:codeend:)

Attach:silmaring7_4.png


Roti laiuse ja kõrguse eraldi saame kätte meetoditega @@get_width()@@ ja @@get_height()@@. Proovi paigutada rott täpselt ekraani keskele, kasutades seda meetodit, et arvutada paigutuse koordinaadid. (320, 240) ei ole õige siin vastus.

(:codestart python gutter='false':)
>>> rott.get_width()
116
>>> rott.get_height()
72
(:codeend:)

!!!!Tekst
Mängudes on tavaliselt teksti, mis näitab vestluseid, õpetusi, skoori, menüüd jpm. Pygame võimaldab teksti kuvada samamoodi nagu piltegi. 

Kõigepealt tuleb valida font, milles teksti kuvada ja teha sellest objekt funktsiooniga @@pygame.font.SysFont()@@. Esimeseks parameetriks läheb fondi nimi sõnena ja teiseks läheb fondi suurus. Kui fondi nimeks valida None, siis valib Pygame vaikimisi fondi. Funktsiooniga Font() saab ka fondi võtta failist. See on kasulik, kui fonti ei pruugi olla teiste arvutis.

(:codestart python gutter='false':)
font = pygame.font.SysFont("Comic Sans MS", 24)
font = pygame.font.SysFont(None, 32)
font = pygame.font.Font("Futura-Bold-Italic.ttf", 40)
(:codeend:)

Kasutades fonti saame meetodiga @@render()@@ luua pinna objekti. Sulgudesse läheb tekst sõnena, tõeväärtus @@True@@ ning teksti värv. Tõeväärtuse kohta võib lugeda [[https://www.pygame.org/docs/ref/font.html#pygame.font.Font.render | dokumentatsioonist]]. Loodud objekt on põhimõtteliselt pilt ehk seda saab meetodiga @@blit()@@ paigutada ekraanile. Loome teksti "Tere, maailm!" musta fondiga ning paigutame selle koordinaatidele (40, 40).

(:codestart python gutter='false':)
tekst = font.render("Tere, maailm!", True, (0, 0, 0))
ekraan.blit(tekst, (40, 40))
(:codeend:)

Attach:silmaring7_5.png


Kogu kood siiamaani:

(:codestart python gutter='false':)
import pygame

ekraan = pygame.display.set_mode((640, 480))
pygame.init()

ekraan.fill((128, 255, 212))

rott = pygame.image.load("rott.png")
ekraan.blit(rott, (100, 200))

font = pygame.font.SysFont(None, 40)
tekst = font.render("Tere, maailm!", True, (0, 0, 0))
ekraan.blit(tekst, (40, 40))

pygame.display.flip()

while True:
    sisend = pygame.event.poll()
    if sisend.type == pygame.QUIT:
        break

pygame.quit()
(:codeend:)

Pinna objekte saab moondada: pööramine, peegeldamine, suurendamine jne. Uuri dokumentatsiooni: [[https://www.pygame.org/docs/ref/transform.html | pygame.transform]].

Harjuta ekraanile joonistamist. Leia vastused küsimustele:

* Kas ühte pinda on võimalik mitu korda ekraanile joonistada?
* Kas pinda saab paigutada ekraanist väljapoole?
* Mis juhtub, kui joonistada pilt enne ja täita ekraan mingi värviga hiljem?
* Mis käskudega saab kätte peegelpildi ja pööramised?
* Kuidas võiks toimuda animeerimine?

!!!Animeerimine
Siiani oleme joonistanud ekraanile ühe kaadri. Et saada paigutatud objekte liikuma, ei saa me lihtsalt muuta nende asukohti: me peame iga kaadri algusest peale joonistama. See võib kõlada ebaintuitiivsena, aga Pygame töötabki just niimoodi. 

Jätame alles roti ekraanile, aga liigutame tema joonistamise meie põhitsüklisse, mis programmi käimas hoiab.

(:codestart python gutter='false':)
import pygame

ekraan = pygame.display.set_mode((640, 480))
pygame.init()

rott = pygame.image.load("rott.png")

while True:
    sisend = pygame.event.poll()
    if sisend.type == pygame.QUIT:
        break
    
    ekraan.fill((128, 255, 212))
    ekraan.blit(rott, (100, 200))
    pygame.display.flip()

pygame.quit()
(:codeend:)

Kui programm käivitada, siis midagi väga muutunud ei ole. Rott on ikka paigal, ta lihtsalt joonistatakse igal tsükli käigul uuesti. See on kasulik, sest nüüd me saame näiteks muuta igal käigul roti asukohta.

Salvestame programmi alguses roti koordinaadid mingitesse muutujatesse, näiteks @@rott_x@@ ja @@rott_y@@, algul olgu need 100 ja 200. Nüüd igal tsükli korral suurendame rott_x muutujat ühe võrra ja käivitame programmi uuesti.

(:codestart python gutter='false':)
rott_x = 100
rott_y = 200

...
    
    rott_x += 1
(:codeend:)

Kui see programm käivitada, siis rott tõepoolest liigub väga kiiresti paremale ja jääbki liikuma. Proovi täitmiskäsk @@ekraan.fill()@@ tsüklist ära võtta ja ainult programmi algusesse panna. Mis juhtub?

Saime roti liikuma, aga mitte väga hästi. Probleem on praegu selles, et tsükli käik ei ole kuidagi ajaliselt piiratud: see kestab täpselt nii kaua, kui Pythonil läheb selle jooksutamiseks. See on esiteks väga lühike aeg: peaksime roti koordinaati liigutama palju vähem (nt 0.1 pikslit) iga kord. Teiseks: see võib erineda erinevates arvutites ja isegi samas arvutis erinevatel aegadel. Tahame, et mäng jookseks kõikides arvutites samamoodi igal hetkel. 

Selle probleemi parandamiseks kasutame Pygame'i kella. Loome programmi alguses uue kella objekti:

(:codestart python gutter='false':)
kell = pygame.time.Clock()
(:codeend:)

Iga tsükli alguses peaks kell tiksuma ehk kutsume välja meetodi @@tick()@@. Sulgudesse läheb arv, mitu kaadrit peaks sekundis kuvama. Valime 60, sest enamik kuvaritest värskenduvad 60 korda sekundis. Selle meetodi käivitamisel ootab programm umbes 1/60 sekundit.

(:codestart python gutter='false':)
dt = kell.tick(60)
(:codeend:)

See meetod tagastab, mitu millisekundit on kulunud eelmisest kella tiksumisest. 60 kaadri sekundis puhul see on enamasti 16 või 17. Seda on väga kasulik teada, sest kui arvuti näiteks kiilub korraks kinni ja mõnda kaadrit joonistatakse natuke kauem, siis meile on see teada ja me saame seda oma mängus arvestada nii, et mängu tegevus ei kiiluks kinni, vaid liiguks vastavalt sellele, kui palju aega on kulunud. Kui kaader peaks kestma 16 millisekundit ja kestab hoopis 32, siis mängu tegevus peaks sellel kaadril liikuma 2 korda kiiremini. Selle abil tagame ka selle, et mäng jookseb kõikidel arvutitel sama kiiresti. 

Seega peame igal liikumisel arvestama kulunud aega (muutujat @@dt@@). Roti liikumisel korrutame sellega läbi.

(:codestart python gutter='false':)
rott_x += 0.25 * dt
(:codeend:)

0.25 on siin roti kiirus, mis väljendab mitu pikslit me tahame, et rott liiguks millisekundis. 0.25 pikslit millisekundis on teisendatult 250 pikslit sekundis. Mitme sekundiga jõuab rott ekraanist välja, kui ekraani laius on 640 ja roti algne x-koordinaat on 100?

Kogu kood:

(:codestart python gutter='false':)
import pygame

ekraan = pygame.display.set_mode((640, 480))
pygame.init()

kell = pygame.time.Clock()

rott = pygame.image.load("rott.png")
rott_x = 100
rott_y = 200

while True:
    dt = kell.tick(60)
    
    sisend = pygame.event.poll()
    if sisend.type == pygame.QUIT:
        break
    
    ekraan.fill((128, 255, 212))
    ekraan.blit(rott, (rott_x, rott_y))
    pygame.display.flip()
    
    rott_x += 0.25 * dt
    
pygame.quit()
(:codeend:)

Attach:silmaring7_6.gif


Proovime veel animatsiooni huvitavamaks teha. Kui rott jõuab teisele poole, siis paneme ta ümber pöörama. Selleks salvestame roti kiiruse eraldi muutujasse.

(:codestart python gutter='false':)
roti_kiirus = 0.25
...
...
...
    rott_x += roti_kiirus * dt
(:codeend:)

Kui roti parem ots jõuab paremalt poolt ekraanist välja, siis lihtsalt teeme tema kiiruse negatiivseks. Teeme sama, kui roti vasak ots jõuab vasakult poolt ekraanist välja.

(:codestart python gutter='false':)
if (rott_x + rott.get_width()) > 640 or rott_x < 0:
    roti_kiirus *= -1
(:codeend:)

Kui roti kiirus on negatiivne, siis tema x-koordinaat väheneb igas tsükli käigus ja ta liigub vasakule.

Pöörame ka roti ümber. Rohkem infot [[https://www.pygame.org/docs/ref/transform.html#pygame.transform.flip | dokumentatsioonis]].

(:codestart python gutter='false':)
    rott = pygame.transform.flip(rott, True, False)
(:codeend:)

Kogu kood:

(:codestart python gutter='false':)
import pygame

ekraan = pygame.display.set_mode((640, 480))
pygame.init()

kell = pygame.time.Clock()

rott = pygame.image.load("rott.png")
rott_x = 100
rott_y = 200

roti_kiirus = 0.25

while True:
    dt = kell.tick(60)
    
    sisend = pygame.event.poll()
    if sisend.type == pygame.QUIT:
        break
    
    if rott_x > (640 - rott.get_width()) or rott_x < 0:
        roti_kiirus *= -1
        rott = pygame.transform.flip(rott, True, False)
    
    ekraan.fill((128, 255, 212))
    ekraan.blit(rott, (rott_x, rott_y))
    pygame.display.flip()
    
    rott_x += roti_kiirus * dt
    
pygame.quit()
(:codeend:)

Rott nüüd põrkab lõputult akna parema ja vasaku ääre vahel. Proovi panna rott ringi liikuma, nagu vana DVD-mängija ekraanisäästja: kui jõuab ääreni, siis põrkab. Vihje: liikuval kahemõõtmelisel esemel saab olla 2 kiirust: horisontaalkiirus ja vertikaalkiirus.

Attach:silmaring7_7.gif


!!!Kasutajasisend
Panime roti liikuma, aga tavaliselt saab mänge mõjutada kasutaja. Paneme roti liikuma vastavalt klahvivajutustele!

Siiani oleme tegelikult mingit kasutajasisendit küsinud: kui ristile vajutada, siis mäng läheb kinni. Senist sisendipüüdmist peame natuke muutma. Kahe tsükli käigu vahel võib toimuda mitu sisendit, aga me oleme iga kord ainult ühe küsinud - peaksime kõike kontrollima. Kõik sisendid saab kätte funktsiooniga @@pygame.event.get()@@. Kuna need peab tsükliga läbi käima, siis @@break@@ viiks meid ainult sellest tsüklist välja. Asendame selle @@sys.exit()@@ väljakutsega, mis lõpetab programmi töö. 

(:codestart python gutter='false':)
for sisend in pygame.event.get():
    if sisend.type == pygame.QUIT:
        sys.exit()
(:codeend:)

Samamoodi, nagu kontrollime ristile vajutamist, saame kontrollida ka klaviatuurivajutusi. Selle jaoks tuleb kontrollida, kas sisendi tüüp on @@pygame.KEYDOWN@@. Kui on, siis saame vaadata selle klahvi väärtust. Nooleklahvid on näiteks @@pygame.K_UP@@, @@pygame.K_DOWN@@, @@pygame.K_LEFT@@, @@pygame.K_RIGHT@@. Vastavalt nendele võiksime rotti liigutada. Erinevaid nuppe saab kätte [[https://www.pygame.org/docs/ref/key.html | dokumentatsioonist]]. 

(:codestart python gutter='false':)
    for sisend in pygame.event.get():
        if sisend.type == pygame.QUIT:
            sys.exit()
        elif sisend.type == pygame.KEYDOWN:
            if sisend.key == pygame.K_UP:
                    rott_y -= roti_kiirus * dt
            elif sisend.key == pygame.K_DOWN:
                rott_y += roti_kiirus * dt
            elif sisend.key == pygame.K_LEFT:
                rott_x -= roti_kiirus * dt
            elif sisend.key == pygame.K_RIGHT:
                rott_x += roti_kiirus * dt
(:codeend:)

Kui nupule vajutada, siis rott tõepoolest liigub… natuke. Kui tahame, et nuppu all hoides rott jääkski liikuma, siis võiksime koordinaatide muutmise asemel hoopis roti kiiruseid muuta ning igal tsükli käigul rotti liigutada vastavalt kiirustele. 

(:codestart python gutter='false':)
roti_kiirus = 0.25
roti_x_kiirus = 0
roti_y_kiirus = 0

...
            if sisend.key == pygame.K_UP:
                roti_x_kiirus = 0
                roti_y_kiirus = -roti_kiirus
            elif sisend.key == pygame.K_DOWN:
                roti_x_kiirus = 0
                roti_y_kiirus = roti_kiirus
            elif sisend.key == pygame.K_LEFT:
                roti_x_kiirus = -roti_kiirus
                roti_y_kiirus = 0
            elif sisend.key == pygame.K_RIGHT:
                roti_x_kiirus = roti_kiirus
                roti_y_kiirus = 0
...

    rott_x += roti_x_kiirus * dt
    rott_y += roti_y_kiirus * dt
(:codeend:)

Kui nüüd nupule vajutada, siis rott jääbki liikuma. See võib mõnes mängus olla soovitud, näiteks ussimängus, aga vaatame, kuidas nupust lahti laskmisel rott seisma jääks. Saame vaadata, et kui sisenditüüp on @@pygame.KEYUP@@ ehk klahvist lastakse lahti, siis nullime kiirused vastavalt klahvile, mis lahti lasti. Kogu kood siiani:

(:codestart python gutter='false':)
    for sisend in pygame.event.get():
        if sisend.type == pygame.QUIT:
            sys.exit()
        elif sisend.type == pygame.KEYDOWN:
            if sisend.key == pygame.K_UP:
                roti_y_kiirus = -roti_kiirus
            elif sisend.key == pygame.K_DOWN:
                roti_y_kiirus = roti_kiirus
            elif sisend.key == pygame.K_LEFT:
                roti_x_kiirus = -roti_kiirus
            elif sisend.key == pygame.K_RIGHT:
                roti_x_kiirus = roti_kiirus
        elif sisend.type == pygame.KEYUP:
            if sisend.key == pygame.K_UP or sisend.key == pygame.K_DOWN:
                roti_y_kiirus = 0
            elif sisend.key == pygame.K_LEFT or sisend.key == pygame.K_RIGHT:
                roti_x_kiirus = 0
(:codeend:)

Selle programmiga on veel probleeme. Kui hoida all paremat noolt, seejärel hoida all vasakut noolt ja lasta parem nool lahti, siis rott jääb seisma, kuigi vasak nool on veel all. Saame igal hetkel kontrollida vajutatud klahve funktsiooniga @@pygame.key.get_pressed()@@. See on sõnastik, mille võtmeteks on klahvid ja väärtusteks on tõeväärtused, kas nuppu hoitakse all või mitte. 

(:codestart python gutter='false':)
import pygame
import sys

ekraan = pygame.display.set_mode((640, 480))
pygame.init()

kell = pygame.time.Clock()

rott = pygame.image.load("rott.png")
rott_x = 100
rott_y = 200

roti_kiirus = 0.25
roti_x_kiirus = 0
roti_y_kiirus = 0

while True:
    dt = kell.tick(60)
    
    for sisend in pygame.event.get():
        if sisend.type == pygame.QUIT:
            sys.exit()

    vajutatud = pygame.key.get_pressed()
    if vajutatud[pygame.K_UP]:
        roti_y_kiirus = -roti_kiirus
    elif vajutatud[pygame.K_DOWN]:
        roti_y_kiirus = roti_kiirus
    else:
        roti_y_kiirus = 0
    
    if vajutatud[pygame.K_LEFT]:
        roti_x_kiirus = -roti_kiirus
    elif vajutatud[pygame.K_RIGHT]:
        roti_x_kiirus = roti_kiirus
    else:
        roti_x_kiirus = 0
            
    rott_x += roti_x_kiirus * dt
    rott_y += roti_y_kiirus * dt
    
    ekraan.fill((128, 255, 212))
    ekraan.blit(rott, (rott_x, rott_y))
    pygame.display.flip()
(:codeend:)

Attach:silmaring7_8.gif


Mõningaid probleeme veel leidub. Kui hoida all nii üles- kui allanoole klahv, siis liigub rott alati ülespoole, sest seda kontrollitakse esimesena. Roti kiirus peaks igas suunas olema võrdne, aga diagonaalis liikudes on ta kiirem. Praeguseks on aga liikumine piisavalt hea, liigume edasi. 

!!!Juust
Mis mäng on mäng ilma eesmärgita? Lisame mängu juustu, mille rott peab kätte saama.

Laadi alla @@juust.png@@ ülevalt ja tuleta meelde, kuidas laadida see mängu ning joonistada see. Juustul võiks olla ka koordinaadid muutujatena, sarnaselt rotile. 

Attach:silmaring7_9.png


Kui proovida juustu peale liikuda, siis midagi ei juhtu. Paneme kokkupuutumisel juustu juhuslikku asukohta hüppama!

Kuidas me kontrollime kokkupuudet? Saaksime manuaalselt vaadata, kui roti koordinaadid on juustu koordinaatide lähedal, aga see on palju tööd ning Pygame lubab seda palju lihtsamalt teha.

Kui me paigutame pinna ekraanile meetodiga @@blit()@@, siis see tegelikult tagastab meile ühe ristküliku objekti ([[https://www.pygame.org/docs/ref/rect.html | Rect]]). See on kasulik objekt, millelt saab kätte pildi koordinaate: keskpunkt, nurgad, servade keskpunktid, laius, kõrgus jne. Muuseas saab sellega kontrollida kahe ristküliku kokkupuutumist üksteisega, kasutades @@colliderect()@@ meetodit. Liigutame kokkupuutumisel juustu uuele juhuslikule asukohale.

(:codestart python gutter='false':)
juust_rect = ekraan.blit(juust, (juust_x, juust_y))
rott_rect = ekraan.blit(rott, (rott_x, rott_y))
   
if rott_rect.colliderect(juust_rect):
    juust_x = random.randint(0, 640 - juust.get_width())
    juust_y = random.randint(0, 480 - juust.get_height())
(:codeend:)

Attach:silmaring7_10.gif


Lõplik kood kommentaaridega:

(:codestart python gutter='false':)
import pygame
import sys
import random
# Loome akna ning käivitame Pygame'i
ekraan = pygame.display.set_mode((640, 480))
pygame.init()
# Alustame Pygame'i kella
kell = pygame.time.Clock()
# Laadime sisse roti pildi
rott = pygame.image.load("rott.png")
# Algväärtustame roti asukoha
rott_x = 100
rott_y = 200
# Kui kiiresti rott liigub klahvivajutustel
roti_kiirus = 0.25
# Kui kiiresti rott parajasti liigub
roti_x_kiirus = 0
roti_y_kiirus = 0
# Laadime sisse juustu pildi
juust = pygame.image.load("juust.png")
# Algväärtustame roti asukoha
juust_x = 400
juust_y = 200
# Põhitsükkel, mis hoiab akent lahti
while True:
    # Tiksume kella (ootame umbes 1/60 sekundit)
    # Salvestame kulunud aja eelmisest tiksumisest millisekundites
    dt = kell.tick(60)
    
    # Käime läbi kõik sisendid eelmisest tiksumisest saati
    for sisend in pygame.event.get():
        # Kui on soovitud aken sulgeda, sulgeme programmi
        if sisend.type == pygame.QUIT:
            sys.exit()
    # Võtame kõik parajasti allavajutatud klahvid
    vajutatud = pygame.key.get_pressed()
    # Kui üles- või allanool on all, määrame roti vertikaalkiiruse 
    if vajutatud[pygame.K_UP]:
        roti_y_kiirus = -roti_kiirus
    elif vajutatud[pygame.K_DOWN]:
        roti_y_kiirus = roti_kiirus
    else:
        # Vastasel juhul peatame roti vertikaalselt
        roti_y_kiirus = 0
  
    # Kui vasak- või paremnool on all, määrame roti horisontaalkiiruse
    if vajutatud[pygame.K_LEFT]:
        roti_x_kiirus = -roti_kiirus
    elif vajutatud[pygame.K_RIGHT]:
        roti_x_kiirus = roti_kiirus
    else:
        # Vastasel juhul peatame roti horisontaalselt
        roti_x_kiirus = 0
    # Liigutame roti asukohta vastavalt kiirustele
    # Korrutame liikumist kulunud ajaga, et rott liiguks sama
    # palju sõltumatult vahele jäänud kaadritest
    rott_x += roti_x_kiirus * dt
    rott_y += roti_y_kiirus * dt
    
    # Täidame akna akvamariinsinise värviga
    # Sellega kustutame ära kõik, mis siiamaani ekraanil on
    ekraan.fill((128, 255, 212))
    
    # Joonistame pildid nende koordinaatidele
    # Saame nende ristküliku objektid
    juust_rect = ekraan.blit(juust, (juust_x, juust_y))
    rott_rect = ekraan.blit(rott, (rott_x, rott_y))
    
    # Kui juustu ja hiire ristkülikud puutuvad kokku
    if rott_rect.colliderect(juust_rect):
        # Liigutame juustu juhuslikele koordinaatidele
        juust_x = random.randint(0, 640 - juust.get_width())
        juust_y = random.randint(0, 480 - juust.get_height())
    
    # Värskendame ekraani
    pygame.display.flip()
(:codeend:)

Kui Pythonis klasside kasutamine on selge, siis võiks igale objektile teha enda klassid, millel on isendiväljad suurustest ja kiirustest ning meetodid liigutamise ja muude tegevuste jaoks: [[https://stackoverflow.com/a/35642955/12123296 | https://stackoverflow.com/a/35642955/12123296]] 

!!!Kokkuvõte
Vaatasime, kuidas Pygame töötab, joonistasime kujundeid, pilte ja teksti, panime pildi liikuma, liigutasime ise pilti klahvivajutustega, kontrollisime kahe eseme kokkupuudet ja saime lõpuks valmis midagi, mida saab enam-vähem mänguks nimetada. Graafiliste mängude loomise alustalad on nüüd all, aga väga palju jäi käsitlemata. 

Et edasi õppida, pakub Pygame erinevaid õpetusi oma [[https://www.pygame.org/wiki/tutorials | veebilehel]]. Tasub ka uurida Pygame'i [[https://www.pygame.org/tags/all | projektide kogumikku]]. Sinna on postitatud palju erinevaid Pygame'iga tehtud mänge koos lähtekoodidega (''Releases'' all), mida tasub uurida.

Ülikoolis saab arvutigraafikaga ja mängudega jätkata ainetes "Arvutigraafika" ([[https://ois2.ut.ee/#/courses/MTAT.03.015/details | MTAT.03.015]]) ning "Arvutimängude loomine ja disain" ([[https://ois2.ut.ee/#/courses/MTAT.03.263/details | MTAT.03.263]]).

!!!Enesekontrolliküsimused
(:includeurl https://courses.cs.ut.ee/LTAT.03.001/2020_fall/uploads/Main/silmaring7_1.html width=100% height="230px" border="0" :)
(:includeurl https://courses.cs.ut.ee/LTAT.03.001/2020_fall/uploads/Main/silmaring7_2.html width=100% height="230px" border="0" :)
(:includeurl https://courses.cs.ut.ee/LTAT.03.001/2020_fall/uploads/Main/silmaring7_3.html width=100% height="230px" border="0" :)

!!!Ülesanded
1. Täienda peatüki jooksul loodud mängu skooriga, mis loeb juustu kättesaamise arvu. Kuva see skoor kuskil mänguaknas tekstina. 

Attach:silmaring7_11.gif

2. Kirjuta Pygame'iga animatsioon, kus mingi pilt või kujund liigub sinusoidi trajektoori või ringjoone järgi.

3. Kirjuta ise Pygame'iga üks mäng, millel on mingi eesmärk. Graafika joonistada ise või võtta Internetist. Viimase puhul tuleb lähtekoodis kommentaarina allikale viidata. Mõned ideed:

* Labürindist pääsemise mäng. Labürindi võib näiteks võtta tekstifailist, sarnaselt [[https://courses.cs.ut.ee/2019/progmaa/spring/Main/Pykkar | pykkarile]]. 
* Lihtne [[https://flappybird.io/ | Flappy Birdi]] kloon. Gravitatsiooni asemel võib vabalt ise kontrollida üles-alla liikumist.
* Esemed lendavad mängija suunas, kes peab eest ära hüppama, et ellu jääda.
