Labas, Pasauli!

Žaidimo ciklas

VP
Vilius P.
2024-02-21

(angl. game loop)

PyGame dokumentacija

Jau pasiruošėme pradėti programuoti žaidimą. Toliau medžiaga remsis PyGame oficialia dokumentacija. Dokumentacija atlieka labai svarbų vaidmenį kuriant ir prižiūrint programinę įrangą. Joje galima rasti aprašytas bibliotekos galimybes, pamokas, apribojimu, efektyvaus naudojimo patarimų.

Pirmas žaidimų langas

Dabar paleiskime pirmą kodą, kuris pateiktas dokumentacijoje. Šio kodo pagalba, mes paleisime žaidimų langą.

.py
# Example file showing a basic pygame "game loop"
import pygame

# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True

while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # fill the screen with a color to wipe away anything from last frame
    screen.fill("purple")

    # RENDER YOUR GAME HERE

    # flip() the display to put your work on screen
    pygame.display.flip()

    clock.tick(60)  # limits FPS to 60

pygame.quit()

Turėjo atsidaryti tuščias purpurinis stačiakampis programos langas. Toliau paanalizuokime šį kodą eilutę po eilutę. Su pirmąją elute kode, mes importuojame pygame biblioteka, kad galėtume ją vėliau panaudoti kode.

.py
import pygame

Toliau inicializuojame pygame su pygame.init() (dokumentacija). Iškvietus šią funkciją, inicializuojami visi pygame moduliai. Toliau sukuriamas ekrano kintamasis, kaip argumentą pateikiame šio ekrano dydį (dokumentacija). Šį kintamąjį geriau būtų įsivaizduoti, kaip tuščią drobę, kurioje galima piešti figūras, animacinius objektus (ang. sprites) ir tekstą. Reiktų suprasti, kad „piešimą“ atliksime programuodami. Paskutinėje eilutėje sukuriame laikrodžio Clock objektą, šis naudojamas „laikui stebėti“. Laikrodis taip pat atlieka keletą funkcijų, padedančių valdyti žaidimo kadrų spartą (dokumentacija).

.py
# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True

Prieš pat ciklą while, sukuriame bool tipo kintamąjį running = True, kuris nurodys mums, ar žaidimas vis dar veikia. Toliau seka pagrindinė žaidimo kodo blokas - žaidimo ciklas. Šiame bloke aprašomas kiekvieno žaidimo momento (angl. tick) veiksmai, komandos. Šis ciklas veiks tol, kol running kintamasis bus True. Ciklai while aprašomi taip:

.py
while sąlyga:
    veiksmas1()
    veiksmas2()
    ...
    veiksmasN()

Žaidimo cikle pirmiausia patikrinami žaidimo įvykis, ar žaidimas išjungiamas ( event.type == pygame.QUIT). Visus pygame įvykius rasite dokumentacijoje. Kaip yra išjungiamas žaidimo langas (paspaudžias X mygtukas), tokiu atveju mes nustatome running = False ir taip baigiamas žaidimo ciklas.

.py
# ...
running = True

while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # ...

Ryškios spalvos violetinį ekraną padarome su screen.fill(color, ...) funkcija. Šiuo atveju užpildoma spalva purple, kuri yra iš anksto sukurta. Spalvas galima nurodyti iš sąrašo arba nurodydami raudonos, žalios, mėlynos spalvų įverčius iki 255, toks formatas dar vadinamas RGB. Pavyzdžiui vietoj "purple" įrašę (0, 0, 255), gautume mėlynos spalvos ekraną. Mėlynos spalvos ekraną taip pat galima gauti įrašę vietoj argumento "blue".

.py
    # ...code before

    # fill the screen with a color to wipe away anything from last frame
    screen.fill("purple")

    # ...code after

Braižymas, piešimas žaidimo lange vyksta netiesiogiai. Pirmiausia, visi grafiniai skaičiavimai, vaizdų piešimai vyksta ne ekrane, o ant surface. Atlikus visas grafinių vaizdų transformacijas, pokyčius, piešimus, šis surface perkeliamas į ekraną (dokumentacija). Tai padaroma su žemiau pateikta kodo eilute:

.py
    # ...code before

    # flip() the display to put your work on screen
    pygame.display.flip()
    
    # ...code after

Žaidimuose vientisas vaizdas gaunamas rodant daug kadrų iš eilės. Atskirų kadrų nematome, kadangi vaizdas atnaujinamas daug kartų per sekundę. Rodiklis rodantis kiek kartų kadrų parodoma per sekundę vadinamas kadrai per sekundę (angl. frames per second, fps). Šį rodiklį, mes limituosime iki 60, kad išvengtume perteklinio kompiuterio resursų, žaidimas būtų stabilesnis.

.py
    # ...code before

    clock.tick(60)  # limits FPS to 60
    
    # ...code after

Pozicija žaidimų lange

Prieš nagrinėjant kitą dokumentacijoje pateiktą pavyzdį, aptarsime, kaip nurodoma objektų (primityvių figūrų, paveiksliukų, pelės žymeklio ir kt.) pozicija žaidimo lange. Pasileiskime anksčiau pateiktą, bet modifikuotą pavyzdį:

.py
# Example file showing a basic pygame "game loop"
import pygame

# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True

while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        # Mouse click coordinates
        if event.type == pygame.MOUSEBUTTONDOWN:
            x = pygame.mouse.get_pos()[0]
            y = pygame.mouse.get_pos()[1]
            print(f"(x,y)=({x},{y})")

    # fill the screen with a color to wipe away anything from last frame
    screen.fill("purple")

    # RENDER YOUR GAME HERE

    # flip() the display to put your work on screen
    pygame.display.flip()

    clock.tick(60)  # limits FPS to 60

pygame.quit()

Paspaudinėkite ant žaidimo lango su pelyte, konsolėje turėtumėte pamatyti kažką panašaus į tai . Modifikacija leidžia paspaudus ant kažkurios vietos lange, konsoliniame lange parodo, kurioje pozicijoje buvo paspausta su pelyte. Ši pozicija nurodoma dviem reikšmėmis: ir koordinatėmis. Koordinatė nurodo, kiek objektas (šiuo atveju pelės žymeklis paspaudimo metu) nutolęs nuo kairiojo lango krašto, tai nepriklauso nuo to, kaip žemai, ar aukštai tas objektas tas yra. Koordinatė y nurodo, kiek objektas nutolęs nuo viršutiniojo lango krašto, tai nepriklauso, kiek kairėn ar dešinėn tas objektas yra. Kuo didesnė koordinatės reikšmė, tuo objektas yra labiau dešinėje. Kuo didesnė koordinatės reikšmė, tuo objektas yra žemiau. Tokius ir koordinačių pokyčius galima atvaizduoti su koordinačių plokštuma. Ši šiek tiek skiriasi nuo tos, kurios mokoma mokyklos kurse (stačiakampės koordinačių sistemos, dekarto plokštumos). Šią vaizduosime konkrečiu atveju - dabartinės žaidimo lango, kuris yra 1280 pikselių ilgio ir 720 pikselių pločio. Tik pakeisime žaidimo fono spalvą į pilką.

Jau turėtumėte žinoti, kad daugelis dalykų programavime skaičiuojami nuo nulio. Koordinačių reikšmės taip pat prasideda nuo nulio. Todėl galinės koordinačių reikšmės yra vienu mažesnės negu nustatytos reikšmės. Nustatėme, kad lango ilgis būtų pikselių ilgio, bet dešiniausios koordinatės reikšmė yra . Jeigu daug išbandinėjote spaudinėjimą ant žaidimo lango ir stebėjote, kaip keičiaisi koordinatės, galėjote pastebėti, kad tiek x, tiek y reikšmės visada būna sveiki skaičiai. Jeigu bandysite nustatyti objekto koordinates su trupmeniniu skaičiu, tai šio skaičiaus dalis po kablelio bus ignoruojama, pašalinama (angl. truncated). Pavyzdžiui taps , taps ir t.t.

Objektas gali turėti ir neigiamas koordinates arba tokias, kurios yra didesnės negu lango ilgis ir plotis. Tokiu atveju objektas bus piešiamas kažkur tai už lango. Tokį atvejį mes pamatysime vėliau. Dabar paanalizuokime kitą dokumentacijoje esantį kodo pavyzdį.

Įvesties įvykiai ir jų apdorojimas

Įvykius jau naudojome anksčiau. Pavyzdžiuose galėtjo pamatyti event.type == pygame.MOUSEBUTTONDOWN ir event.type == pygame.QUIT.

Kuriant žaidimus su Pygame, kiekvieną kartą paspaudus klaviatūros klavišą ar pelės mygtuką įvyksta įvykis (angl. event). Pygame registruoja kiekvieną įvykį ir talpina jį sąraše. Kuriant žaidimą, reikės dirbti su šiuo įvykių sąrašu ir pagal šiuos įvykius vykdyti instrukcijas.

Pavyzdžiui, paspaudus mygtuką , Pygame užregistruoja šį paspaudimo įvykį ir prideda į sąrašą, kaip „aukštyn mygtukas buvo paspaustas“. O tada galima patikrinti, ar toks mygtukas buvo paspaustas ir, pavyzdžiui, galima padaryti, kad veikėjas pašoktų.

Kodas

Įvestiems įvykiams panagrinėti, analizuosime kodą, kuris pateiktas dokumentacijoje pateiktas sekantis:

.py
# Example file showing a circle moving on screen
import pygame

# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
dt = 0

player_pos = pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)

while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # fill the screen with a color to wipe away anything from last frame
    screen.fill("purple")

    pygame.draw.circle(screen, "red", player_pos, 40)

    keys = pygame.key.get_pressed()
    if keys[pygame.K_w]:
        player_pos.y -= 300 * dt
    if keys[pygame.K_s]:
        player_pos.y += 300 * dt
    if keys[pygame.K_a]:
        player_pos.x -= 300 * dt
    if keys[pygame.K_d]:
        player_pos.x += 300 * dt

    # flip() the display to put your work on screen
    pygame.display.flip()

    # limits FPS to 60
    # dt is delta time in seconds since last frame, used for framerate-
    # independent physics.
    dt = clock.tick(60) / 1000

pygame.quit()

Pasileiskite, turėtumėte pamatyti apskritimą viduryje ekrano, pasinaudojus WASD klavišais turėtumėte galėti jį valdyti.

Kodo analizė

Pirmosios kelios eilutės sutampa su prieš tai analizuotų kodu, tik čia papildomai inicializuojamas kintamasis dt = 0. Šio kintamojo reikšmę kodui aptarsime vėliau, o kol kas laikykite tai skirtumas tarp dviejų laiko momentų.

Toliau sutinkame ir sekančio kintamojo sukūrimą player_pos. Šio kintamojo vardas reiškia player position. Šį kintamąjį naudosime saugoti žaidėjo pozicijai. Jo pradinė reikšmė nustatoma žaidimo lango centro koordinatės. Nors šio kintamojo tipo atikmuo matematikoje yra vektorius, kol kas tai laikyime, kaip taško koordinates (x, y). Vector2 naudojamas:

  • veiksmams su judėjimu;
  • objektų fizikai aprašyti;
  • susidūrimų identifikavimui;
  • krypčiai ir atstumui nustatyti;

Šie dalykai turėtų skambėti sudėtingai, tai kol kas į juos nesigilinkime ir analizuokime kodą toliau.

.py
# ...
    pygame.draw.circle(screen, "red", player_pos, 40)
# ...

Nuspalvinus ekraną purpurine spalva, ekrane nupiešiamas raudonas apskritimas, kurios spindulys yra , o pozicija tokia pat, kaip player_pos reikšmė - pirmame žaidimo kadre, tai žaidimo lango centras. Daugiau informacijos apie skritulių, apskritimų braižymą rasite dokumentacijoje.

.py
# ...
    # ...
    keys = pygame.key.get_pressed()
    if keys[pygame.K_w]:
        player_pos.y -= 300 * dt
    if keys[pygame.K_s]:
        player_pos.y += 300 * dt
    if keys[pygame.K_a]:
        player_pos.x -= 300 * dt
    if keys[pygame.K_d]:
        player_pos.x += 300 * dt

    # ...

    # limits FPS to 60
    # dt is delta time in seconds since last frame, used for framerate-
    # independent physics.
    dt = clock.tick(60) / 1000

# ...

Pateiktas kodo fragmentas leidžia valdyti apskritimą į visas kryptis. Su keys = pygame.key.get_pressed() eilute, sukuriamas kintamasis, per kurį galima pasiekti visus mygtukų paspaudimo įvykius. Sukurtas kintamasis yra dictionary tipo. Kol kas šio tipo neanalizuosime, bet reiktų žinoti, jeigu norime sužinoti, ar mygtukas paspaustas reikia naudoti [ ] skliaustelius, juose nurodant mygtuką. Pavyzdžiui:

.py
keys = pygame.key.get_pressed()
is_w_pressed = keys[pygame.K_w] 
print(f"is w key pressed: ${is_w_pressed}")
# Print the state of the 'W' key. The output will be:
# "is W key pressed: True"
# or
# "is W key pressed: False"
# depending on whether the 'W' key is currently pressed.

Tokios išraiškos reikšmė yra bool. Visus mygtuko reikšmes rasite pygame.key dokumentacijoje. Anksčiau pateiktame pavyzdyje, matome daug sąlygos sakinių, kuriuose tikriname ar vienas iš WASD mygtukų paspaustas. Jeigu paspautas:

  • W, tai prie žaidėjo pozicijos y koordinatės atimama reikšmė;
  • S, tai prie žaidėjo pozicijos y koordinatės pridedamas reikšmė;
  • A, tai prie žaidėjo pozicijos x koordinatės atimama reikšmė;
  • D, tai prie žaidėjo pozicijos x koordinatės pridedamas reikšmė;

Skaičius yra greitis, šis greitis išreikštas, šiuo atveju, pikseliais per sekundę. Daugiklis dt yra kažkoks tai kiekis sekundėmis. Kaip šis apskaičiuojamas pasiaiškinsime vėliau. Pakeitus žaidėjo pozicijos kintamojo reikšmę, jo pozicija ekrane nesikeičia, mes pakeičiame tik kintamojo reikšmę. Ši reikšmė naudojama kitame cikle, ankstesnėje eilutėje pygame.draw.circle(screen, "red", player_pos, 40) - piešiamas toks pat apskritimas, bet jau kitoje pozicijoje. Tokiu būdu apskritimo pozicija žaidimo lange kinta.

Kodo pavyzdyje naudojama ir sutrumpinta sumos, atimties sintaksė. Vietoje:

.py
player_pos.y = player_pos.y + 300 * dt

Užrašoma (atkreipkite dėmesį į + priešais =):

.py
player_pos.y += 300 * dt

Daugiau apie tokią sintaksę galite pasiskaityti čia.

Kadravimo dažnis

Šis rodiklis dar gali būti pavadintas kadrais per sukundę (angl. Frames Per Second, FPS). Kadravimo dažnis rodo, kiek kartų per sekundę žaidimas atnaujina ir atvaizduoja naują kadrą. Didesnis FPS paprastai užtikrina sklandesnę animaciją ir sklandesnį žaidimą, o dėl mažesnio FPS žaidimas gali atrodyti trūkčiojantis ar vėluojantis.

Kuriant žaidimą, kadrai pasirodo ne vienodu intervalu, vienas gali pasirodyti per 0.1 sekundės, kitas per 0.05. Tarkime turime judėjimo greitį , kol kas, tegul šis greitis reiškią nueitą kelią per kadrą. Pasižiūrėkime, kiek taškas nukeliaus atstumo ( - delta) per 5 kadrus (kadrų atvaizdavimo laikas parinktas atsitiktinai), jeigu šis juda greičiu :

KadrasKadras atvaizdavimo laikas Atstumas
000
10.110
20.2210
30.3110
40.3910
50.4910

5 kadrai buvo parodyti per sekundes, taškas nukeliavo pikselius (px). Skaičiuojant realesniais pasaulio matmenimis - pikseliais per sekunde , greitis yra .

Dabar paanalizuokime kitą situaciją, vėl 5 kadrai, vėl tas pats greitis, bet dabar mūsų sistema apkrauta labiau, procesorius užimtas daugiau, o kadrai sugeneruojami lėčiau.

KadrasKadras atvaizdavimo laikas Atstumas
000
10.210
20.4310
30.6110
40.7110
50.8310

5 kadrai buvo parodyti per sekundes, taškas nukeliavo tiek pat pikselių - . Suskaičiuokime greitį : .

Vienu atveju turime, kad per sekundę taškas nukeliaus pikselius, kitu atveju, kai sistema veikia lėčiau, pikselius. Tai nėra gerai, nes visi judėjimai priklausys nuo to, kaip greitai veikia sistema, kiek ji apkrauta, koks geras procesorius (CPU) ar vaizdo plokštė (GPU). Tokiai problemai spręsti, greitį reikia pririšti prie realaus pasaulio laiko. Pygame tai padaroma skaičiuojant laiką praėjusį nuo praeito kadro:

.py
    # ...
    # limits FPS to 60
    # dt is delta time in seconds since last frame, used for framerate-
    # independent physics.
    dt = clock.tick(60) / 1000

# ...

Kintamasis dt tariamas delta t. Šis skirtumas tarp kadrų atvaizdavimo laiko matematikoje gali būti užrašytas kaip , čia praėjęs laikas tarp dviejų kadrų, - kadro prieš atvaizdavimo laikas, - kadro atvaizdavimo laikas. Tarkime turim du kadrus, vienas atvaizduotas žaidimo 10 sekundę, sekantis atvaizduotas žaidimo 10.2 sekundę, tai skirtumas tarp šių kadrų laiko yra sec. O tai reiškia, kad laikas nuo vieno kadro atvaizdavimo iki kito kadro atvaizdavimo yra sekundės.

Pygame kode clock.tick(60) vienetai yra milisekundės, todėl toliau padalinamas iš 1000, kad būtų gautos sekundės.

Toliau pažiūrėkime, kaip anksčiau pateiktoje lentelėje keisis skaičiai, kai skaičiuosime nuo to laiko, kuris praėjo tarp kadrų atvaizdavimo. Greitis .

KadrasKadras atvaizdavimo momentas Atstumas
0000
10.2
20.43
30.61
40.71
50.83

Šiuo atveju nukeliautas atstumas . Kad ir kaip keistųsi kadrų atvaizdavimo laikas, mūsų realus greitis pastovus ir nekintantis, nepriklausomai nuo sistemos pajėgumų.

Šis greitis anksčiau pateiktame kodo pavyzdyje yra pikseliai per sekundę, kuris yra padauginamas iš laiko tarp kadrų player_pos.y += 300 * dt. Kaip ir anksčiau, tai galima išreikšti ir formule .

Spalvos

Nustatinėjome ekrano spalvą, apskritimo spalvą naudojantis tos spalvos pavadinimą. Tokių spalvų Pygame pakete yra 665. Spalvų ir joms priskirtų pavadinimų sąrašą rasite dokumentacijoje. Šiuolaikiniai monitoriai sugeba atvaizduoti 16,777,216 (8 bit spalvų skiriamoji geba) ar 1,073,741,824 (10 bit spalvų skiriamoji geba) spalvas. Nors ir detaliai nesiaiškinsime, kas yra monitoriaus skiriamoji geba, reiktų suprasti, kad 665 spalvų neišnaudoja visų monitoriaus galimybių.

RGB

Kuriant su Pygame, spalvas galime dar aprašyti naudojantis RGB (ir ne tik) formatu. Šis formatas nėra išskirtinai naudojamas čia, tai vienas dažniausiai naudojamų formatų aprašant spalvas. RGB formate, spalvos apibrėžiamos trimis komponentėmis: raudonos (angl. Red), žalios (angl. Green), mėlynos (angl. Blue) spalvų reikšmėmis, nuo 0 iki 255. Kartais dar naudojamas šio formato plėtinys RGBA, kuriame įtraukta Alpha reikšmė, su kuria nurodomas spalvos permatomumas, o tai leidžia sukurti skaidrumo efektą.

Spalvoms aprašyti naudojami pygame.Color(...) objektai, kurie buvo naudojami anksčiau buvusiuose pavyzdžiuose. Kuriant šį objektą nurodomos (jeigu naudojamas RGB formatas) trys natūraliųjų skaičių reikšmės nuo 0 iki 255 (įskaitant). Jeigu norima nurodyti spalvos permatomumą, reikia nurodyti ir ketvirtą reikšmę, kuri taip pat turi būti nuo 0 iki 255 (įskaitant).

.py
import pygame

color_rgb = pygame.Color(102, 205, 170) # r, g, b are numbers between 0 and 255 inclusive.
# The color above is same as the color below 
color_named = pygame.Color("aquamarine3") # Named color from documentation.
# The color above is same as the color below 
color_with_alpha = pygame.Color(102, 205, 170, 255) # The fourth value is alpha. 255 means that color is fully opaque.

Pabandykime su šiomis spalvomis nupiešti tris skritulius:

.py
import pygame

SCREEN_COLOR = pygame.Color(140, 107, 136)

color_rgb = pygame.Color(102, 205, 170) # r, g, b are numbers between 0 and 255 inclusive.
# The color above is same as the color below 
color_named = pygame.Color("aquamarine3") # Named color from documentation.
# The color above is same as the color below 
color_with_alpha = pygame.Color(102, 205, 170, 255) # The fourth value is alpha. 255 means that color is fully opaque.

# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
surface = pygame.Surface((1280, 720), pygame.SRCALPHA)

clock = pygame.time.Clock()

first_circle_pos = pygame.Vector2(100, screen.get_height() / 2)
second_circle_pos = pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)
third_circle_pos = pygame.Vector2(screen.get_width() - 100, screen.get_height() / 2)

running = True

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
  
    screen.fill(SCREEN_COLOR)
     
    pygame.draw.circle(surface, color_rgb, first_circle_pos, 40)
    pygame.draw.circle(surface, color_named, second_circle_pos, 40)
    pygame.draw.circle(surface, color_with_alpha, third_circle_pos, 40)
 
    screen.blit(surface, (0,0))
    pygame.display.flip()
    
    dt = clock.tick(60)


pygame.quit()

Paleidus programą, gavome tris vienodos spalvos apskritimus, nors spalvos aprašytos skirtingai.

Dabar pakeiskime pirmo ir trečio apskritimo spalvų reikšmes, pavyzdžiui taip:

.py
color_rgb = pygame.Color(200, 205, 170) # r value is changed from 102 to 200.
# The color above is different than the color below 
color_named = pygame.Color("aquamarine3") # Named color from documentation.
# The color above is different than the color below 
color_with_alpha = pygame.Color(102, 205, 170, 25) # Alpha value is changed from 255 to 25.

Ekrane matome, vieną smėlinės spalvos apskritimą, kuriam buvo pakeista raudonos spalvos komponentės reikšmė iš 102 į 200. Viduriniam apskritimui nieko nepadarėme. Trečiasis tapo labiau permatomas, o tai padarėme pakeitus alpha reikšmę iš 255 į 25.

Parenkant tris spalvų reikšmes, sunku įsivaizduoti kokią spalvą gausime, todėl galima pasinaudoti color picker įrankiu, kurių apstu internete. Pavyzdžiui:

Kiti formatai

Atsidarę kažkurį color picker įrankį ir pasirinkę spalvą, galima ne tik pamatyt RGB spalvos formatu aprašytą spalvą, bet ir keletą kitų.

Kuriant pygame.Color(...) objektą, vietoje trijų RGB spalvų reikšmių, galime naudoti ir HEX formato spalvą. Pavyzdžiui:

.py
import pygame

# Pale Glaucous-Green color in HEX format
# In French: Blanc Balustrade
color_rgb = pygame.Color("#e1e4d5")

Šis formatas taip pat populiarus aprašant spalvas. Vietoje to, kad rašytume skaičius dešimtainėje sistemoje (nuo 0 iki 9), spalvų komponentės nurodomos šešioliktainiais skaičiais. Skaičiai visada prasideda # simboliu, toliau rašant 6 šešioliktainius skaitmenis nuo 0 iki F (tai atitinka nuo 0 iki 255). Šie šeši skaitmenys iš tikrųjų yra trys poros, kuriomis nurodomos raudonos, žalios, mėlynos spalvos intensyvumo (stiprumo) reikšmės. Pavyzdžiui:

  • #000000 reiškia juodą (nėra raudonos, žalios arba mėlynos spalvos).
  • #FFFFFF reiškia baltą (visų grynų trijų spalvų kombinacija).
  • #FF0000 reiškia gryną raudoną (maksimalus raudonos spalvos stiprumas, nulis žalios ir mėlynos spalvos).
  • #00FF00 reiškia gryną žalią (maksimalus žalios spalvos stiprumas, nulis raudonos ir mėlynos spalvos).
  • #0000FF reiškia gryną mėlyną (maksimalus mėlynos spalvos stiprumas, nulis raudonos ir žalios spalvos).

Kaip ir su RGB, taip ir HEX formatą galima praplėsti ir įrašyti du skaitmenis, kurie nurodytų spalvos alpha reikšmę - skaidrumo lygį.

Pygame galima sutikti ir kitus color picker įrankyje matomos formatus: CMYK, HSV, HSL (šie irgi naudojami ne tik šiame įrankyje). Jeigu įdomu, galite pasiskaityti apie šiuos formatus plačiau kitur arba dokumentacijoje.

lerp()

Susipažinkime su pygame.Color objekto lerp() metodo dokumentacija:

Progamavime raktažodžiu lerp žymima tiesinės intepoliacijos funkcija. Turėtų skambėti sudėtingai, bet toliau tik bus paprasčiau. Tarkime turime dvi spalvas ir . Kadangi spalvas sudaro trys komponentės, jas galima laikyti kaip taškus trimatėje erdvėje. Įsivaizduokime tiesę tarp šių dviejų taškų ir bet kokį tašką ant tos tiesės:

To taško vieta ant tiesės priklausys nuo reikšmės, o šis yra tarp 0 (įskaitant) ir 1 (įskaitant), t.y. arba . Šį reiktų suprasti, kaip kelio dalį, kurioje yra mūsų naujasis taškas, šį pažymėkime . Pavyzdžiui:

  • Kai , tai taškas yra toje pačioje vietoje, kaip ir taškas ;
  • Kai , tai taškas yra toje pačioje vietoje, kaip ir taškas ;
  • Kai , tai taškas yra viduryje tiesės, vidurkelyje tarp taško ir .

Tai kontroliuoja, kurioje vietoje yra naujasis taškas tarp dviejų kitų taškų. Grafiškai tai atrodytų taip:

Šiek tiek konkretumo - pasiimkime dvi spalvas ir paskaičiuokime pabandykime interpoliuoti keletą taškų tarp šių dviejų spalvų. Tarkime turi spalvą ir spalvą . Apskaičiuokime spalvą , kai yra nuo iki , kas . Spalvų komponenčių reikšmes apskaičuosime su šiomis formulėmis:

;

;

;

Nauja spalva

Lentelėje pateikti dešimtainiai skaičiai, bet raudonos, žalios, mėlynos spalvos įverčiai turi sveikieji skaičiai, todėl galutinėje spalvoje šios vertės yra apvalintos.

Dabar pasižiūrėkime, kaip spalvų intepoliaciją galima panaudoti Pygame aplikacijoje. Sukursime jau iš anksčiau žinomą apskritimą, bet šio spalva bus interpoliuota reikšmė, kuri priklausys , o šis priklausys nuo žaidimo eigos. Sukurkime tris spalvas: fono, apskritimo pirmąją spalvą, apskritimo antrąją spalvą:

.py
import pygame

SCREEN_COLOR = (140, 107, 136)
COLOR_A = pygame.Color(225, 228, 213)
COLOR_B = pygame.Color(102, 205, 170)

Dabar sukurkime kitus kintamuosius, kuriuos kūrėme ir anksčiau:

.py
# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
dt = 0
circle_pos = pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)

Sukurkime žaidimo ciklą ir nustatykime fono ciklą:

.py
while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    
    # fill the screen with a color to wipe away anything from last frame
    screen.fill(SCREEN_COLOR)
     
    # flip() the display to put your work on screen
    pygame.display.flip()

    # limits FPS to 60
    # dt is delta time in seconds since last frame, used for framerate-
    # independent physics.
    dt = clock.tick(60) / 1000

pygame.quit()

Paleiskite, turite matyti vienspalvį tuščia langą. Dabar pasinaudokime matematika ir apskaičiuokime naudojantis laiku nuo žaidimo paleidimo pradžios pygame.time.get_ticks(). Kadangi reikia mums funkcijos, kurios reikšmės kartotųsi, t.y. būtų periodinė, naudosime (būtų galima naudoti ir ). Funkcijos reikšmės visada bus nuo (įskaitant) iki (įskaitant):

Kadangi mums reikalingos , turime pamodifikuoti , kad gautume tinkamas reikšmes, tai padarysime naudojantis funkcijų transformacijas. Šį grafiką pakelsime per vieneto į viršų ir suspausime kartus ašies atžvilgiu. Gausime, kad . Tokios funkcijos grafikas atrodys taip:

Tokia dabar galima naudoti nustant reikšmę.

.py
while running:
    # ...

    # fill the screen with a color to wipe away anything from last frame
    screen.fill(SCREEN_COLOR)
     
    t = 0.5*math.sin(pygame.time.get_ticks()*0.01)+0.5        
    # ...

Vietoje sinuso argumento įrašomas laikas praėjęs nuo žaidimo pradžios ir taip gaunamos reikšmės nuo 0 iki 1. Taip pat dar šis laikęs laikas praėjęs nuo žaidimo pradžios sumažinamas 100 kartų (pygame.time.get_ticks()*0.01), kad interpoliacija vyktų lėčiau. Dabar šį kintamąjį t panaudokime interpoliuojant spalvas ir piešant spalvotą apskritimą.

.py
import pygame
import math

SCREEN_COLOR = (140, 107, 136)
COLOR_A = pygame.Color(225, 228, 213)
COLOR_B = pygame.Color(102, 205, 170)

# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
dt = 0
circle_pos = pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)

while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # fill the screen with a color to wipe away anything from last frame
    screen.fill(SCREEN_COLOR)
     
    t = 0.5*math.sin(pygame.time.get_ticks()*0.01)+0.5    
    color_lerp = COLOR_A.lerp(COLOR_B, t)
    
    pygame.draw.circle(screen, color_lerp, circle_pos, 80)

    # flip() the display to put your work on screen
    pygame.display.flip()

    # limits FPS to 60
    # dt is delta time in seconds since last frame, used for framerate-
    # independent physics.
    dt = clock.tick(60) / 1000

pygame.quit()

Turėjote gauti spalvas bekeičiantį apskritimą:

O dabar galite savarankiškai išbandyti ir paanalizuoti šį pamodifikuotą kodą:

.py
import pygame
import math
from random import randrange

SCREEN_COLOR = (140, 107, 136)

# freely chosen value
COLOR_CHANGE_THRESHOLD = 0.001
color_a = pygame.Color(225, 228, 213)
color_b = pygame.Color(102, 205, 170)

# Set up the font
font_size = 24
pygame.font.init()
my_font = pygame.font.SysFont('arial', font_size)

# pygame setup
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
dt = 0

circle_radius = 80
line_length = circle_radius*4
circle_pos = pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)
t_line_pos = pygame.Vector2(screen.get_width() / 2,
                            screen.get_height() / 2 + 150)


while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # fill the screen with a color to wipe away anything from last frame
    screen.fill(SCREEN_COLOR)

    t = 0.5*math.sin(pygame.time.get_ticks()*0.01)+0.5
    color_lerp = color_a.lerp(color_b, t)

    pygame.draw.circle(screen, color_lerp, circle_pos, 80)

    # ---------------------------------------------
    # Color disco
    # --------------------------------------------
    # If t is smaller than threshold
    # change color_b
    if t < COLOR_CHANGE_THRESHOLD:
        rand_color = (randrange(255), randrange(255), randrange(255))
        color_b.update(rand_color)

    # if t is bigger than 1-threshold
    # change color_a
    if t > 1 - COLOR_CHANGE_THRESHOLD:
        rand_color = (randrange(255), randrange(255), randrange(255))
        color_a.update(rand_color)

    # ---------------------------------------------
    # Progress bar
    # --------------------------------------------
    # Calculate start and end points for a horizontal line
    start_pos = pygame.Vector2(t_line_pos.x - line_length / 2, t_line_pos.y)
    end_pos = pygame.Vector2(t_line_pos.x + line_length / 2, t_line_pos.y)
    end_pos = start_pos.lerp(end_pos, t)
    pygame.draw.line(screen, "white", start_pos, end_pos, 5)
    # Render the text showing the value of t
    text_surface = my_font.render(f't={t:.2f}', True, (255, 255, 255))
    # Position the text above the line's start position
    text_pos = (start_pos.x, start_pos.y - font_size - 10)  # Adjust as needed
    # Blit the text surface onto the screen
    screen.blit(text_surface, text_pos)

    # -----------------------------------------------
    pygame.display.flip()

    dt = clock.tick(60) / 1000

pygame.quit()

Užduotys

1. Pakeiskite žaidimo lango fono spalvą

Naudojantis RGB formatu, nustatykite fonui kitą spalvą:

.py
import pygame

color = pygame.Color(r,g,b) #r, g, b are numbers between 0 and 255 inclusive

Spalvai pasirinkti galite naudoti color picker.

Dokumentacija: Surface.fill, pygame.color.

2. Sugrįžimas į pradžios tašką

Padarykite taip, kad paspaudus space, skritulys grįžtų į ekrano centrą.

3. Greitis

Sukurkite penkis greičių lygius, paspaudus , greičio lygis turėtų padidėti, o skritulys turėtų judėti greičiau. Paspaudus , greičio lygis turėtų sumažėti, o skritulys judėti žemiau. Pasiekus 5 greičio lygį, jo lygis nebegali kilti. Esant 0 lygiui, negalima mažinti greičio lygių, t.y. greičio lygis negali būti neigiamas. 0 greičio lygis yra bazinis, tik įsijungus žaidimą. Greičio lygius pasirinkite savo nuožiūrą.

Pasiūlymas: pirmiausia išveskite į ekraną keičiamą greitį su print(...) ir tik tada pakeiskite realiai.

4. Greičio indikatorius

Kad nereikėtų spėlioti greičio lygio iš matomo vaizdo arba išvesti jo į konsolę, padarykite greičio indikatorių. Greičio indikatoriui atvaizduoti naudokite 5 kvadratus nupaišytus jūsų pasirinktoje vietoje ekrane. Indikatoriaus braižymo veiksmai turi būti aprašyte funkcijoje, suteikite jai prasmingą pavadinimą.

Kvadratą galima nupaišyti su keturkampio piešimo funkcija draw.rect(). Pavyzdys:

.py
import pygame

pygame.init()
screen = pygame.display.set_mode((1280, 720))

while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # fill the screen with a color to wipe away anything from last frame
    screen.fill("purple")

    # Rectangle parameters
    rect_color="white"
    top_left_x = 10
    top_left_y = 20
    side_length = 5
    side_length = 15

    # To draw rectangle, you need to provide:
    # screen (surface),
    # color of rectangle,
    # the top left x coordinate, the top left y coordinate, width, height.
    # Rectangle docs: https://www.pygame.org/docs/ref/rect.html#pygame.Rect
    pygame.draw.rect(screen, rect_color, (top_left_x, top_left_y, side_length, side_length))

    # flip() the display to put your work on screen
    pygame.display.flip()

pygame.quit()

5. Greičio indikatorius klasėje

Žaidimo kodo eilučių skaičius gali išaugti iki 10000..., o su tiek eilučių dirbti viename faile nebūtų patogu. Šiai problemai spręsti pasinaudosime klasėje ir šiek tiek išskaidysime žaidimo logiką į klases - indikatorių iškelsime į klasę. Susikurkite naują failą speed_indicator.py. Ten sukurkite naują tris metodus:

  • konstruktorių, kuriame nurodysime pradinius parametrus (greitį, pozicija);
  • metodą greičio lygio pakeitimui (pvz. update_speed, change_speed_level, increase_speed, decrease_speed ar pnš.);
  • piešimo metodą, o šis, kaip argumentą, turės screen (ekraną).

Nepamirškite main.py faile importuoti naujos klasės (pvz.: from speed_indicator import SpeedIndicator).

Galite žemiau pateiktą kodą naudoti, kaip klasės griaučius:

.py
import pygame

class SpeedIndicator:
    """
    A class to display a speed indicator as a series of squares on a Pygame screen,
    where each square represents a unit of speed.
    
    Attributes:
        indicator_rect (pygame.Rect): The rectangle defining the first indicator square's position and size.
        level (int): The current speed level, determining the number of squares to display.
    """
    
    def __init__(self, position, initial_speed_level=0):
        """
        Initializes a SpeedIndicator object.
        
        Args:
            position (tuple): The (x, y) coordinates of the indicator's top right corner.
            initial_speed_level (int): The speed level at which to start. Defaults to 0.
        """
        pass
    
    def increase_speed(self):
        """Increases the speed level by one."""
        pass
        
    def decrease_speed(self):
        """Decreases the speed level by one, ensuring it doesn't go below zero."""
        pass
    
    def draw(self, screen: pygame.Surface):
        """
        Draws the speed indicator on the provided screen.
        
        Args:
            screen (pygame.Surface): The Pygame surface where the indicator should be drawn.
        """
        pass

Sukurta
su meile

Kontaktai

2024 — Vilius P.