Sündmused

Mängu mängimiseks peab saama kasutaja (mängija) anda korraldusi mängule. Sellised korraldused (näiteks klaviatuuri või hiire klahvi vajutamised) on sündmused (events). Mängutsüklis me tegelikult juba kasutasime sündmuste kontrolli selleks, et akna sulgemisel lõpetataks mäng ära.

PyGame hoiab sündmusi järjekorras (queue). Sündmused lisatakse järjekorda nende tekkimise järjekorras. Sündmusi saab lugeda selles järjekorras, nagu need on järjekorda lisatud. Ehk järjekorrast võetakse järgmisena kõige varasemana salvestatud sündmus (FIFO - first in first out). Sündmuste järjekorral on limiit. Kui järjekord saab täis, siis uusi sündmusi sinna ei lisata (viga selle kohta ka ei anta). Seega mõistlik oleks, kui mängutsükli igal sammul loetakse järjekorrast sündmused. Vastasel juhul, kui sündmused kuhjuvad, võib operatsioonisüsteem pidada rakendust (mängu) hangunuks ning programmi töö lõpetatakse.

Selleks, et kõiki sündmusi lugeda, saab kasutada pygame.event.get() funktsiooniga. See tagastab järjendi Event tüüpi objektidest. get() funktsioonile saab ette anda ka sündmuste tüübi, milliseid sündmusi soovime saada. get() meetod eemaldab kõik tagastatud sündmused sündmuste järjekorrast. Soovitatav on igal tsükli sammul sündmusi lugeda. Kui mingil põhjusel otseselt sündmusi pole vaja kasutada, siis on soovitatav kasutada funktsiooni pygame.event.pump(). See tagab kõikide sündmuste korrektse käsitlemise ning sündmuste järjekord tehakse tühjaks.

Igal sündmusel on tüüp. PyGame toetab väga palju erinevaid tüüpe. Aga vaatame siinkohal peamisi tüüpe, millega mängu juhtida. Need on klaviatuuri ja hiire sündmused.

Klaviatuuri sündmused

Klaviatuurilt saab lugeda klahvivajutusi, täpsemalt klahvi allavajutamise (KEYDOWN) ja klahvi lahtilaskmise (KEYUP) sündmusi. Seda tüüpi sündmustel on kaasas ka info klahvi enda kohta (atribuut key).

Vaatame näidet, kus ring liigub ekraanil ringi vastavalt noolte vajutustele:

import pygame

pygame.init()
# create a window
screen = pygame.display.set_mode((800, 600))

running = True
player_x = 100
player_y = 100
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # closing window finishes the game loop
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                player_y -= 10
            if event.key == pygame.K_DOWN:
                player_y += 10
            if event.key == pygame.K_LEFT:
                player_x -= 10
            if event.key == pygame.K_RIGHT:
                player_x += 10

    # fill screen with white color
    screen.fill((255, 255, 255))

    # draw a blue circle at player's position (x, y) with radius 50
    pygame.draw.circle(screen, (0, 0, 255), (player_x, player_y), 50)

    # everything drawn will be visible
    pygame.display.flip()

Loome alguses kaks muutujat, mis tähistavad mängija x ja y koordinaate (vastavalt player_x ja player_y muutujad). Anname neile alguses väärtused 100 ja 100. Ja nüüd vastavalt klahvivajutustele hakkame mängijat liigutama. Hetkel selles näites on mängija lihtsalt üks ring. Aga see võib vabalt olla mingi pilt või ristkülik vms.

Tsükkel, kus käiakse läbi kõik sündmused, omab eraldi kontrolli selle kohta, kas sündmuse tüüp on pygame.KEYDOWN. Kui on, siis teame, et loetud sündmus event on klahvivajutus. Edasi kontrollime, mis on selle sündmuse klahv (event.key). Võrdluseks kasutame PyGame’is eeldefineeritud klahvide konstante, mille saame kätte pygame moodulist ning mille nimi algab K_. Näiteks noolteklahvide konstandid on:

  • pygame.K_UP - ülesnool

  • pygame.K_DOWN - allanool

  • pygame.K_LEFT - vasakule nool

  • pygame.K_RIGHT - paremale nool

Vastavalt klahvivajutusele muudame mängija koordinaati. Näiteks kui vajutatakse ülesklahvi, siis mängija y-koordinaat väheneb 10 võrra.

Harjutus

Siin näites saab ekraanilt välja liikuda. Võid proovida koodi muuta nii, et kui koordinaadi väärtus läheb liiga suureks või väikeseks, siis seda enam ei muudeta. Selliselt ei saa pall akna raamides välja liikuda.

Antud kood töötab ja saab mängijat liigutada. Aga kui paned tähele, siis klahvi all hoides midagi ei toimu. Asi on selles, et KEYDOWN sündmus registreeritakse vaid sellel hetkel, kui klahv alla vajutatakse. Kui seda all hoitakse, siis ühtegi sündmust ei registreerida. Üks võimalus selle „lahendamiseks“ on see, et kui klahv vajutatakse alla, määratakse mängijale liikumiskiirus. Kui klahv lastakse lahti (KEYUP), siis määratakse kiiruseks 0 (ehk mängija jääb seisma).

Lisame praegu kiiruse vaid x-suunalisele liikumisele:

import pygame

pygame.init()
# create a window
screen = pygame.display.set_mode((800, 600))

running = True
player_x = 100
player_y = 100
speed_x = 0
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # closing window finishes the game loop
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                speed_x = -1
            if event.key == pygame.K_RIGHT:
                speed_x = 1
        if event.type == pygame.KEYUP:
            speed_x = 0

    if speed_x != 0:
        player_x += speed_x

    # fill screen with white color
    screen.fill((255, 255, 255))

    # draw a blue circle at player's position (x, y) with radius 50
    pygame.draw.circle(screen, (0, 0, 255), (player_x, player_y), 50)

    # everything drawn will be visible
    pygame.display.flip()

Lisasime ühe täiendava muutuja speed_x, mis algselt on 0. Kui toimub klahvivajutus (KEYDOWN), siis määratakse speed_x väärtus. Kui klahv lastakse lahti (KEYUP), siis määratakse speed_x väärtus nulliks.

Mõtle

Miks pole KEYUP tingimuse all vaja eraldi kontrollida, kas lahti lasti vasakule või paremale nool?

Harjutus

Proovi eelnevat koodi muuta nii, et ka y-suunaliselt toimub sarnane liikumine (kuni klahvi hoitakse all, siis mängija liigub; kui klahv lastakse lahti, siis mängija jääb seisma).

Klahvivajutusi on võimalik saada ka pygame.key.get_pressed() funktsiooniga. See tagastab sõnastiku, kus võti on klahvi kood (pygame.K_ konstantide seast) ja väärtus on tõeväärtus, kas seda klahvi on vajutatud.

Seda kasutades saab ümber kirjutada mängija liikumise vastavalt klahvivajutusele järgnevalt:

keys = pygame.key.get_pressed()
if keys[pygame.K_UP]:
    player_y -= 1
if keys[pygame.K_DOWN]:
    player_y += 1
if keys[pygame.K_LEFT]:
    player_x -= 1
if keys[pygame.K_RIGHT]:
    player_x += 1

keys muutujasse pannakse sõnastik, kus klahvidele on vastavuses kas True või False. Kui klahv on hetkel all, siis vastava võtme puhul väärtus on True.

Siin näites on muudetud sammu pikkuseks 1 (algses näites oli see 10) piksel, kuna klahvivajutus kestab tavaliselt mõne hetke ning kogu selle aja vältel on sõnastikus vastava klahvi väärtus tõene ja seetõttu toimub ka liikumine.

Kuigi selline kirjapilt on üldiselt lühem ja võib-olla ka loetavam, siis peab arvestama, et sellise lahenduse puhul ei ole sündmuste järjekord teada. Kui näiteks mängija peab midagi kirjutama, siis vajutuste järjekord ei pruugi siit selge olla (vajutab väga väikese vahega kaks klahvi alla, siis selle tsükli sammu sees võivad mõlemad olla sõnastikus tõese väärtusega, aga kumb enne vajutati pole teada).

Harjutus

Proovi eelnevat koodi muuta nii, et kasutad taustapilti ning kujundi asemel omakorda pilti. See tähendab, et screen.fill() asemel tuleks kasutada blit() funktsiooni. Mõistlik oleks proovida asendada vaid see osa ekraanist, kus midagi on muutunud (kust tuleb „kustutada“ vana objekti pilt).

Hiire sündmused

Hiirelt saab sarnaselt klaviatuurile lugeda hiire klahvivajutusi (MOUSEBUTTONDOWN ja MOUSEBUTTONUP). Lisaks saab lugeda hiire liikumise sündmust MOUSEMOTION. Klahvisündmuste objektis on olemas positsioon (pos), kus hiir oli klahvivajutuse hetkel, ja klahv (button), mida vajutati.

Vaatame näidet, kus ekraanil on pall (ring). Kui selle peal klikkida, liigub pall uuele positsioonile:

import pygame
import random

pygame.init()
# create a window
screen = pygame.display.set_mode((800, 600))

running = True
player_x = 100
player_y = 100
player_radius = 50

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # closing window finishes the game loop
            running = False
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == pygame.BUTTON_LEFT:
            # let's use vector here
            vector = pygame.Vector2(event.pos[0] - player_x, event.pos[1] - player_y)
            if vector.length() <= player_radius:
                player_x = random.randint(0 + player_radius, screen.get_width() - player_radius)
                player_y = random.randint(0 + player_radius, screen.get_height() - player_radius)

    # fill screen with white color
    screen.fill((255, 255, 255))

    # draw a blue circle at player's position (x, y) with radius 50
    pygame.draw.circle(screen, (0, 0, 255), (player_x, player_y), player_radius)

    # everything drawn will be visible
    pygame.display.flip()

Lisaks mängija asukohale (player_x ja player_y) loome muutuja player_radius, mis tähistab ringi raadiust. Kuna kasutame seda raadiust mitmes kohas, siis on mõistlik see muutujaks määrata.

Sündmuste tsüklis kontrollime, kas sündmuse event tüüp on pygame.MOUSEBUTTONDOWN ja vajutatud klahv on hiire vasak klahv (event.button == pygame.BUTTON_LEFT). Nüüd on meil teada, et toimus hiire vasaku klahvi vajutus. Me peame kontrollima, kas klikk toimus ringi sees või mitte. Selleks arvutame hiire positsiooni kauguse ringi keskpunktist.

../../_images/mouse_position_distance_from_circle_center.png

Pildil on kujutatud ring koos keskpunktiga (sinine täpp). Punane täpp on positsioon, kus toimus hiire klikk. Leiame x- ja y-teljel kahe punkti vahe (vastavalt dx ja dy). Nendest kahes väärtusest saame luua vektori (joonisel roheline joon v). Kui selle vektori pikkus on väiksem või võrdne ringi raadiusega (joonisel R), siis järelikult toimus klikk ringi sees. Kui kaugus on raadiusest suurem, siis toimus klikk ringist väljas.

dx arvutamiseks leiame sündmuse (hiirekliki) positsiooni x-koordinaadi (event.pos[0]) väärtuse ja ringi keskpunkti x-koordinaadi (player_x) väärtuse vahe (event.pos[0] - player_x). See vahe võib ka negatiivne olla. Aga vektori pikkuse jaoks ei ole vahet. Sarnaselt arvutama dy vahe. Me võiks vektori asemel ka näiteks Pythagorase teoreemi kasutada, et v pikkus arvutada (kuna dx ja dy vaheline nurk on täisnurk, v on hüpotenuus), aga vektoriga on seda mugavam teha.

Harjutus

Proovi koodi muuta nii, et kui arvutatakse uus ringi asukoht, siis see ei tohiks sattuda sellisesse asukohta, et ta juba jääb hiire positsiooni alla.

Siin võib mõelda nii, et uute koordinaatide arvutmine võiks toimuda lõputus tsüklis. Seda tsüklit korratakse seni, kuni uued koordinaadid ei too ringi hiire alla. Selleks saab peale koordinaatide arvutamist leida nende kaugus hiire positsioonist (näiteks kasutades vektorit). Kui kaugus on piisav, siis lõpetatakse lõputu tsükkel ära. Muul juhul korratakse tegevust uue tsükli sammu sees.


Täpsemat lugemist sündmuste kohta: https://www.pygame.org/docs/ref/event.html