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:

../../_images/surface_example.png

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.

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:

../../_images/background_space.png

Meie tegelase pilt:

../../_images/rocket_small.png

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:

../../_images/move_image.png

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 salvestame rocket 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 seda rocket_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() - loome Clock 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õetakse rocket_rect ristkülik (täpselt see osa, kus rakett praegu on). See võetud ristkülik paigutatakse ekraanile sellele positsioonile, mis on määratud rocket_rect ristkülikuga. Kuna meil taustapilt on paigutatud katma tervet ekraani, siis siin saab täpselt sama ristkülikut kasutada. Teine argument rocket_rect tähistab positsiooni (Rect objektist võetakse vaid ülemaise vasakpoolse punkti koordinaadid), kuhu paigutatakse pilt. Kolmas argument rocket_rect tähistab ristkülikut (seekord koos alguspunkti ning kõrguse ja laiusega), mis „lõigatakse“ välja bg 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älja pygame.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 siis x 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 salvestama rocket_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

Proovi koodi muuta nii, et muutub liikuva objekti alguspositsioon ning liikumiskiirus. Näiteks võiks objekt liikuda ka diagonaalis.

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ülik

  • polygon - hulknurk

  • circle - ring

  • ellipse - ellips

  • arc - kaar

  • line - sirgjoon

  • lines - 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 joonistada

  • color - 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 äärest
    top - pikslite arv ülemisest äärest
    width - ristküliku laius pikslites
    height - ristküliku kõrgus pikslites
  • width - 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"