84 PyGame’i ekraan ja joonistamine
PyGame’is toimub visuaalse pildi joonistamine n-ö joonistamispinnale (surface). Pind on ristküliku kujuline ala, millele saab joonistada ja pilte lisada. pygame.display.set_mode()
funktsiooniga luuakse üks eriline pind, mis tähistab kogu mängu nähtavat osa. Seda loodud pinda kasutatakse, et sinna mängukomponente joonistada.
Pinna sisu luuakse valmis mängutsükli jooksul ning tavaliselt tsükli lõpus näidatakse see välja kasutajale pygame.display.flip()
funktsiooniga. Näiteks mängija tegelane liigub ja vastane liigub. See sisuliselt tähendab, et pinna peal muudetakse mängija pildi/kujundi asukohta ja vastase pildi/kujundi asukohta. Asukoha muutmine tähendab seda, et vana pilt/kujund tuleb “kustutada” ja uus tuleb uuele positsioonile “joonistada”. Kui need sammud on tehtud, siis kutsutakse välja funktsioon, mis näitab muudatused ekraanile. Kui iga muudatus jõuaks kohe ekraanile, toimub väga palju visuaali muutmist (korra kustutatakse tegelane ära ja siis joonistatakse uuesti), mis on mängijale ebamugav. PyGame’i puhul aga teeme muudatuse olekutes ära ning flip()
funktsiooniga näidatakse need korraga välja.
Pindade kasutamine
Kui eelnevalt rääkisime peamisest pinnast, mida näidatakse aknas kasutajale, siis PyGame’is saab luua täiendavaid pindu. Selleks saab kasutada Surface
klassi. See annab võimaluse täpsemalt juhtida, millist osa ekraanist on vaja uuendada. Näiteks võib kogu aken olla jagatud mitmeks osaks. Ühes osas on mäng, teises on menüü, kolmandas on mängija parameetrid. Sellisel juhul menüüd ja mängija parameetreid ei pea pidevalt uuendama. Kui mäng on eraldi pinnal, siis on seda mugavam hallata ja mäng töötab ka kiiremini.
Loodav pind tuleb alati lisada mõne teise pinna peale. Üldiselt lisatakse loodav pind peamise pinna peale (mida pygame.display.set_mode()
funktsiooniga loome). Aga on võimalik ka nii, et peamise pinna peal on pind (näiteks mängupind) ja selle peal on omakorda mõni pind (mängija tegelane või vastase tegelane). Seega pind tuleb lisada teise pinna peale. Lisatav pind jääb ekraanil esiplaanile taustapinna suhtes. Ehk kui peamine pind on must, aga sinna lisatakse valge mängupind, siis valge mängupind katab terves mängupinna ulatuses musta pinna valgega.
Pinna lisamine teise pinna peale toimub funktsiooniga parent_surface.blit(child_surface, (left, top))
. child_surface
pind lisatakse parent_surface
pinna peale asukohale left
pikslit vasakult ja top
pikslit ülevalt äärest. Need koordinaadid on arvestatud parent_surface
pinna suhtes.
Siin on üks koodinäide:
import pygame pygame.init() # create a window screen = pygame.display.set_mode((800, 600)) running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: # closing window finishes the game loop running = False # fill screen with white color screen.fill((255, 255, 255)) # game surface game_surface = pygame.Surface((600, 500)) game_surface.fill((128, 0, 0)) screen.blit(game_surface, (0, 0)) # player info surface player_info_surface = pygame.Surface((200, 500)) player_info_surface.fill((0, 0, 0)) # put it on the right side screen.blit(player_info_surface, (600, 0)) # everything drawn will be visible pygame.display.flip()
Tulemus peaks olema järgmine:
Koodis luuakse game_surface
pind, mis lisatakse ekraanile vasakusse ülemisse äärde (0, 0)
. Seejärel luuakse mängija andmete pind, mis lisatakse mänguala kõrvale. Kuna mänguala laius on 600 pikslit, siis paigutatakse mängija andmete pind 600 piksli kaugusele vasakust äärest.
Nüüd kui mängus midagi muutub, ei pea me tervet akent üle värvima. Piisab vaid mänguala üle värvimisest. Tegelikult saab asju veel optimaalsemalt teha – saab üle värvida vaid selle osa, mis on muutunud. See annab teatud olukorras arvestatava kiirusevõidu. Kuigi esialgu mängutegemise juures ei ole see nii oluline. Kui kohe algusest peale proovid asju optimeerida, võib mängutegemise lõbu ära kaduda. Pigem tegele optimeerimisega siis, kui see probleem tekib.
Mõtle ja nuputa!
Pildi kasutamine
Tavaliselt mängus me ei kasuta igavaid värvilisi kaste, vaid taustapilti. Samamoodi mängu tegelane on pilt. PyGame’is pilt on samamoodi pind (surface). Seega väga palju sellest, mida eelnevalt kirjeldasime, rakendub ka piltide kohta.
Kogu ekraanil paistev visuaal on tegelikult pilt. Vahet pole, kas me joonistame ringe, loome uue pinna või lisame pilte. Lõpuks PyGame joonistab pildi. Pilt ise on tegelikult pikslite kogum. Seega võib öelda, et mängu jooksul me peame muutma pikslite väärtust (värvi) ja seeläbi näeb mängija asju liikumas ja muutumas.
Teeme ühe lihtsa näite, kus meil mängus on taustapilt ja selle peal liigub tegelane. Kuna plaanime teha kosmoseteemalist mängu, siis kasutame järgmisi pilte. Tegelikult pole üldse vahet, mis pilte kasutada – võid vabalt proovida mõne oma pildiga.
Meie taustapilt:
Meie tegelase pilt:
Tegelase pilt on läbipaistva taustaga, et taustapilt paistaks ilusti tema ümbert välja. Muidu pildid ise on ristküliku kujulised. Kui pildi taust ei oleks läbipaistev, siis oleks tegelane ka alati ristküliku kujuline.
Vaatame, kuidas pildi liikumine peaks välja nägema:
Pildil on näha akna raam. Akna sisu on pind, millel võib omakorda olla taustapilt (seda teeme hiljem koodis, selguse mõttes siin on taust valge). Pildi ümber olev punktiirjoon tähistab selle piirjooni. Tegelikult pildil neid jooni näha ei ole.
Alustame sellest, et lisame pildi positsioonile (left, top)
. Need on ka joonisel nooltega tähistatud. Kui tahame raketti liigutada paremale, siis joonistame selle pildi teatud hulga pikslite võrra paremale – näiteks positsioonile (left + 100, top)
. Kui me seda teeme, siis on ekraanil kaks raketti – algne ja liigutatud rakett. Selleks, et kasutaja jaoks oleks näha vaid üks rakett, peame vana “kustutama”. Nagu eelnevalt kirjeldasime, siis toimub pikslite väärtuste muutmine. Selleks, et vasakpoolne rakett ära kaoks, peame selle üle värvima. Kuna hetkel on meil taust valge, siis piisab, kui värvime selle ala valgega. Kui taustal oleks pilt, peaksime selle ala täitma täpselt selle osaga taustapildist. Alguses võib lihtsam olla see, et värvime kogu tausta korraga ära – see on aeglasem, aga ei pruugi esialgu meie mängu mõjutada. Siin paljudes näidetes oleme lihtsuse mõttes terve tausta korraga üle värvinud. Aga teeme siin ühe täpsema näite.
import pygame pygame.init() # create a window screen = pygame.display.set_mode((640, 640)) bg = pygame.image.load("background_space.png") rocket = pygame.image.load("rocket_small.png") # rocket rectangle <rect(0, 0, 64, 62)> rocket_rect = rocket.get_rect() # we will use this rectangle to position the rocket, therefore let's set y value rocket_rect.y = 150 # game surface with background image screen.blit(bg, (0, 0)) # set up a clock in order to control the flow of time clock = pygame.time.Clock() running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: # closing window finishes the game loop running = False # "erase" the current rocket screen.blit(bg, rocket_rect, rocket_rect) # change rocket position (x = x + 2) rocket_rect = rocket_rect.move(2, 0) # draw a rocket to a new position screen.blit(rocket, rocket_rect) # everything drawn will be visible pygame.display.flip() # keep the program running at 30 FPS clock.tick(30)
blit
funktsiooni saab välja kutsuda kujul destination.blit(source, destination_position)
. See tähendab, et destination
pinnale lisatakse source
pind (näiteks pilt) positsioonile destination_position
. Positsioon tähistab ära pikslite arvu vasakult ja ülevalt sihtkoha pinna ülevalt vasakult äärest. Positsioon võib olla kujul (left, top)
, aga seal võib kasutada ka Rect
objekti. Rect
objektil on lisaks ülemise vasakpoolse punkti koordinaadile (left
ja top
) ka laius ja kõrgus. Ristküliku kasutamise puhul võetakse sealt vaid left
ja top
väärtused. Meie näites teeme samamoodi, et kasutame ristkülikut raketi paigutamiseks.
Vaatame, mida eelnev kood täpsemalt teeb:
- Koodinäite alguses loeme sisse kahe pildi andmed. Taustapildi salvestame
bg
muutujasse ja raketi pildi salvestamerocket
muutujasse. rocket_rect = rocket.get_rect()
– küsime raketi pildi ristküliku objekti. See tagastab ristküliku, mille ülemine vasakpoolne tipp on koordinaadil(0, 0)
ning millel on pildi laius ja kõrgus. Seega saame konkreetselt sellise tulemuse:<rect(0, 0, 64, 62)>
. Seega meie pildi laius on 64 pikslit ja kõrgus 62 pikslit.rocket_rect.y = 150
– kuna eelnevalt küsitud ristküliku koordinaat oli(0, 0)
, siis muudame kõrguse ära. Kasutame sedarocket_rect
muutujat selleks, et joonistada rakett ekraanile. Hetkel siis rakett ei alusta päris ülevalt äärest, vaid 150 pikslit altpoolt.screen.blit(bg, (0, 0))
– lisame taustapildi ekraanile positsioonile(0, 0)
. Akna mõõdud oleme valinud vastavalt taustapildile. Seega kogu mänguala täidetakse meie taustapildiga.clock = pygame.time.Clock()
– loomeClock
objekti selleks, et hiljem saaks mängu tempot määrata. See on vajalik selleks, et mängutsükli täitmine ei toimuks liiga kiiresti. Muidu meie rakett liiguks liiga kiiresti ekraanilt välja.screen.blit(bg, rocket_rect, rocket_rect)
– “kustutame” vana raketi pildi. Täpsemalt toimub see, et taustapildilt võetakserocket_rect
ristkülik (täpselt see osa, kus rakett praegu on). See võetud ristkülik paigutatakse ekraanile sellele positsioonile, mis on määratudrocket_rect
ristkülikuga. Kuna meil taustapilt on paigutatud katma tervet ekraani, siis siin saab täpselt sama ristkülikut kasutada. Teine argumentrocket_rect
tähistab positsiooni (Rect
objektist võetakse vaid ülemaise vasakpoolse punkti koordinaadid), kuhu paigutatakse pilt. Kolmas argumentrocket_rect
tähistab ristkülikut (seekord koos alguspunkti ning kõrguse ja laiusega), mis “lõigatakse” väljabg
pildilt. Seega täpselt see osa, mis jääb raketi alla, kopeeritakse taustapildilt ekraanile. Selle tulemusena joonistatakse rakett üle, ehk siis rakett “kustutatakse”. Peale seda rida raketti enam ekraanil näha ei ole (tegelikult seda sammu me päriselt ei näe, kuna kõik toimub näidatakse väljapygame.display.flip()
väljakutsega tsükli lõpus).rocket_rect = rocket_rect.move(2, 0)
– muudame raketi ristküliku positsiooni nii, et ta liigub kaks piksli paremale. Ehk siisx
koordinaati suurendame kahe võrra. Kõrgus jääb paigale (muudetakse 0 piksli võrra).move
meetod ei muuda ristkülikut, vaid loob uue, seepärast peame tulemuse salvestamarocket_rect
muutujasse (kirjutame selle üle).screen.blit(rocket, rocket_rect)
– joonistame raketi pildi uuele positsioonile (mis on eelmise positsiooniga võrreldes 2 piksli võrra paremal).pygame.display.flip()
– kõik tehtud muudatused näidatakse kasutajale.clock.tick(30)
– määrame FPS (frames per second) väärtuse. PyGame vajadusel ootab siin natuke selleks, et tsükli samm ei töötaks liiga kiiresti. Määratud väärtus tähendab, et PyGame ootab piisavalt, et kaadrite arv sekundis ei ületaks seda väärtust.
Mängu tempo määramisest lühike video:
Harjutus
Harjutus
Proovi koodi muuta nii, et eksisteerib ka teine liikuv objekt, mis liigub teises suunas teise kiirusega.
Selleks tuleb lisada teine pilt. Võib ka sama pilti kasutada, aga siis on vaja eraldi Rect
objekti, näiteks enemy_rect = rocket.get_rect()
. Seejärel saab enemy_rect
objektil muuta näiteks x
või y
väärtust ning kasutada seda pildi “kustutamiseks” ning joonistamiseks ekraanile.
Kujundite joonistamine
Joonistamiseks kasutame moodulit pygame.draw
. Selles moodulis on funktsioonid erinevate kujundite joonistamiseks:
rect
– ristkülikpolygon
– hulknurkcircle
– ringellipse
– ellipsarc
– kaarline
– sirgjoonlines
– mitu ühendatud sirgjoont
Nende kujundite joonistamisel antakse funktsioonile kaasa pind, millele kujund joonistada. Ühtlasi saab kaasa anda värvi ja tavaliselt ka joone jämeduse.
Teeme ühe näite, kus joonistame ekraanile ühe sinise ringi ja rohelise ristküliku.
import pygame pygame.init() # create a window screen = pygame.display.set_mode((800, 600)) running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: # closing window finishes the game loop running = False # fill screen with white color screen.fill((255, 255, 255)) # draw a blue circle at (100, 100) with radius 50 pygame.draw.circle(screen, (0, 0, 255), (100, 100), 50) # draw a green rectangle at (200, 100) with width 300, height 100 pygame.draw.rect(screen, (0, 255, 0), (200, 100, 300, 100)) # everything drawn will be visible pygame.display.flip()
Ristküliku joonistamine
pygame.draw.rect(surface, color, rect, width=0)
Parameetrid:
surface
– pind, millele joonistadacolor
– värv (vt Värvid)rect
– ristküliku asukoht ja parameetrid. Need võivad olla järgmisel kujul:(left, top, width, height)
– ennik (tuple) nelja väärtusega;((left, top), (width, height))
– kaks ennikut: esimeses on alguspunkti koordinaadid, teises on ristküliku laius ja kõrgus;Rect
objekt
eelnevalt kasutatud väärtused:left
– pikslite arv vasakult ääresttop
– pikslite arv ülemisest äärestwidth
– ristküliku laius pikslitesheight
– ristküliku kõrgus piksliteswidth
– piirjoone jämedus pikslites. Vaikimisi väärtus on 0. Kui väärtus on 0, siis värvitakse kujund üleni märgitud värviga ära. Muul juhul on värvitud vaid piirjoon vastava jämedusega.
Värvid
Color(r, g, b, a=255)
Värvide esitamiseks kasutakse RGBA koode. RGB tähistab kolme värvi: punane (Red), roheline (Green) ja sinine (Blue). Lühend tuleneb värvide inglisekeelsete sõnade esitähtedest. Neljas täht A tähistab läbipaistvust (Alpha). Vaikimisi a
väärtus on 255
, mis tähendab, et läbipaistvust ei ole. Teine äärmus on 0
, kus värv on 100% läbipaistev. Vahepealsete väärtustega saab osaliselt läbipaistvat värvi luua. RGB väärtused on samamoodi vahemikus 0 - 255
(kaasa arvatud). Väärtus näitab, millise intensiivsusega vastavat värvi kasutatakse. Näiteks (ere)punase värvi jaoks määrame r
väärtuse maksimaalseks (255) ja teised minimaalseks (0).
Color(color_value)
Värvi võib määrata ka sõnena. Võimalikud vormingud:
- värvi nimi, näiteks
"red"
. Võimalikud värvid on välja toodud siin: https://www.pygame.org/docs/ref/color_list.html "#rrggbbaa"
või"#rrggbb"
, kus tähed tähistavad analoogselt punase, rohelise, sinise ja läbipaistvuse väärtusi. Siin kasutatakse kuuteistkümnendnumbreid. Näiteks (ere)punase jaoks:"#ff0000"
või"#FF0000"
.
Mõned näited:
- Punane värv:
(255, 0, 0)
või"#FF0000"
- Roheline värv:
(0, 255, 0)
või"#00FF00"
- Sinine värv:
(0, 0, 255)
või"#0000FF"
- Kollane värv:
(255, 255, 0)
või"#FFFF00"
- Must värv:
(0, 0, 0)
või"#000000"
- Valge värv:
(255, 255, 255)
või"#FFFFFF"
- Hall värv:
(128, 128, 128)
või"#808080"