Pygame

Kategória: Python.

Áttekintés

Ez az oldal a pygame külső könyvtárat ismerteti. Ennek segítségével tudunk játékot fejleszteni.

A játékfejlesztés egy külön terület a szoftverfejlesztésen belül. A pygame segítségével valójában csak egyszerűbb, nagyjából a '80-as éveket idéző játékokat tudunk készíteni, a látványos, 3 dimenziós hatást keltő játékok készítéséhez más, alacsonyabb szintű eszközökre van szükség. Ám a pygame kiválóan alkalmas az első lépések megtételéhez, valamint a játékfejlesztés alapkoncepcióinak megismeréséhez.

Mivel a pygame egy külső könyvtár, először fel kell telepíteni a szokásos módon:

pip install pygame

Ha feltelepítettük, próbáljuk ki a következő parancs segítségével:

python -m pygame.examples.aliens

Ha mindent jól csináltunk, az Aliens nevű játék jelenik meg. Ezzel nagyjából képet kaphatunk arról, hogy mlyen lehetőségeket nyújt ez a rendszer, ill. arról is, hogy mik a korlátai.

Ennek az oldalnak az elkészítésében nagyon sokat segített a https://realpython.com/pygame-a-primer/ leírás.

Esemény ciklus

Áttekintés

Angolul event loop. Ez a játékfejlesztés egyik alapkoncepciója: egy végtelen ciklus, melyben az alábbiak történnek:

  • Az események feldolgozása. Néhány tipikus esemény:
    • billentyű műveletek (adott billentyű lenyomása, elengedése),
    • egérműveletek (pl. kattintás),
    • joystick műveletek,
    • időzítések.
  • Az üzleti logika megvalósítása, pl.:
    • szereplők pozícióinak megváltoztatása,
    • ütközés vizsgálat,
    • stb.
  • A képernyő újrarajzolása: alapból a teljes képernyő újrarajzolódik; mindegyik szereplő letörlődik majd újra ráhelyeződik.
  • Időzítés: ahhoz, hogy a játék élvezhetősége ne függjön a processzor sebességétől, érdemes beállítani azt, hogy milyen gyakorisággal fusson le a ciklusmag.

Példa

Lássunk egy példát! A Scratch űrhajós játékhoz hasonló játékot fogunk készíteni. Ennek az első lépéseit valósítjuk most meg.

import pygame
 
KÉPERNYŐ_SZÉLESSÉG = 480
KÉPERNYŐ_MAGASSÁG = 360
 
pygame.init()
pygame.display.set_mode((KÉPERNYŐ_SZÉLESSÉG, KÉPERNYŐ_MAGASSÁG))
pygame.display.set_caption('Űrhajós játék')
 
fut = True
while fut:
    for esemény in pygame.event.get():
        if esemény.type == pygame.QUIT:
            fut = False
 
    pygame.display.flip()
pygame.quit()

Ha elindítjuk, egy fekete képernyőt látunk:

fekete.png

A kód magyarázata:

  • import pygame: a szokásos be kell tölteni a pygame könyvtárat.
  • pygame.init(): a pygame inicializálása. Kicsit boilerplate kódnak érzem, mert mindegyik pygame játékhoz ki kell írni, de ki kell írni.
  • pygame.display.set_mode((KÉPERNYŐ_SZÉLESSÉG, KÉPERNYŐ_MAGASSÁG)): ezzel adjuk meg a képernyő szélességét és magasságát. Az egy jó programozási gyakrolat, hogy nem közvetlenül a számokat adjuk meg, hanem konstansokat hozunk létre. Mivel a Scratch játékokat utánozzuk, az ottani 480x360 méreteket adtam meg. Figyeljük meg a dupla zárójelet: a külső a paraméter, a belső pedig az elem n-es, azaz tuple (ms néven: módosíthatatlan lista) létrehozásának a zárójele. Ez utóbbi lehetne szögletes zárójel is.
  • pygame.display.set_caption('Űrhajós játék'): ezzel az ablak megnevezését adjuk meg.
  • fut = True és while fut:: ez valójában az esemény ciklus. Angolul a fur helyett azt célszerű írni, hogy running.
  • for esemény in pygame.event.get(): ennek segítségével végig lépked azokon az eseményeket, amelyek az ezt megelőző ciklusmag óta következtek be.
  • if esemény.type == pygame.QUIT: ezzel azt vizsgáljuk, hogy az aktuális esemény az-e, hogy a felhasználó bezárta a programot, azaz a jobb felső sarokban található x-re kattintott-e. "Python mércével mérve" egyébként ez is egy felesleges kód; igazából a bezárásnak automatikusnak kellene lennie.
  • pygame.display.flip(): ez a nem túl informatív nevű függvény rajzolja ki újra a teljes képernyőt.
  • pygame.quit(): a pygame.init() párja: megszűnik az ablak és véget ér a program.

Szereplők

A játékokban a szereplők alapvető fontosságúak. Először megnézzük, hogy hogyan tudunk mi magunk szereplőket létrehozni, majd azt is, hogy a keretrendszer milyen megoldásokat kínál.

Szereplő rajzolása

Az űrhajós játékunkban az űrhajó kezdeti verziója egy álló fehér téglalap lesz. Mielőtt még megvalósítjuk, egy fontos fogalommal meg kell ismerkedünk: ez a felszín, angolul surface, a vonatkozó pygame osztály pedig a pygame.Surface. A felszín téglalap alakú, amelyben található a szereplő.

Lássuk az alábbi kódot:

űrhajó = pygame.Surface((30, 50))
űrhajó.fill((255, 255, 255))
képernyő.blit(űrhajó, ((KÉPERNYŐ_SZÉLESSÉG - űrhajó.get_width()) // 2, KÉPERNYŐ_MAGASSÁG - 70))

Magyarázat:

  • űrhajó = pygame.Surface((30, 50)): ennek segítségével hozzuk létre az űrhajó szereplő felszínét, melynek a szélessége 30, a magassága pedig 50 képpont. Figyeljük meg a dupla zárójelet: a külső a függvényhívás zárójele, a belső pedig a módosíthatatlan listáé, azaz tuple-é.
  • űrhajó.fill((255, 255, 255)): kitöltjük az űrhajó felszínét fehér színnel. A 255, 255, 255 a fehér RGB kódja.
  • képernyő.blit(űrhajó, ((KÉPERNYŐ_SZÉLESSÉG - űrhajó.get_width()) // 2, KÉPERNYŐ_MAGASSÁG - 70)): a blit() hívással tudunk egy felszínt egy másik felszínre helyezni. Az alap képernyő is egy felszín, így rá tudunk helyezni más felszíneket, jelen esetben az űrhajót. A blit() függvényt azon a felszínen kell meghívni, amelyre a másikat helyezni szeretnénk; jelen esetben ez a képernyő. Az első paraméter az a felszín, amit rá szeretnénk helyezni; jelen esetben ez az űrhajó. A második paraméter pedig az a koordináta (x, y), ahova a ráhelyezendő felszín bal fölső sarka kerül. A képernyő szélességéből és magasságából számoljuk ki, hogy alul, kb. középen legyen.
téglalap.png

Szereplő mozgatása

Első körben a szereplőt a következőképpen mozgatjuk: változókba felvesszük annak x és y pozícióját (bár ez utóbbi nem változik). A fő ciklusban figyeljük a nyíl billentyűket, és ha le van nyomva valamelyik, akkor a neki megfelelő koordinátát változtatjuk a megfelelő irányba. Számolással figyeljük azt is, hogy kimegy-e a képernyőről; gondoskodunk kell arról, hogy csak a képernyő széléig mehessen. A kirajzolást a fő ciklusban végezzük.

Az űrhajó x és y koordinátái (fő cikluson kívül):

űrhajó_x_pozíció = (KÉPERNYŐ_SZÉLESSÉG - űrhajó.get_width()) // 2
űrhajó_y_pozíció = KÉPERNYŐ_MAGASSÁG - 70

Billentyűk lenyomott állapotának ellenőrzése és az x koordináta aktualizálása a főciklusban:

    lenyomott_billentyűk = pygame.key.get_pressed()
    if lenyomott_billentyűk[pygame.K_LEFT]:
        űrhajó_x_pozíció -= 10
        if űrhajó_x_pozíció < 0:
            űrhajó_x_pozíció = 0
    if lenyomott_billentyűk[pygame.K_RIGHT]:
        űrhajó_x_pozíció += 10
        if űrhajó_x_pozíció > KÉPERNYŐ_SZÉLESSÉG - űrhajó.get_width():
            űrhajó_x_pozíció = KÉPERNYŐ_SZÉLESSÉG - űrhajó.get_width()

A módszer: a pygame.key.get_pressed() hívással lekérdezzük az összes lenyomott billentyűt, majd megnézzük a számunkra lényegeseket.

Kirajzolás a főciklusban:

    képernyő.blit(űrhajó, (űrhajó_x_pozíció, űrhajó_y_pozíció))

A jobbra ill. balra nyilakkal tudjuk jobbra-balra mozgatni.

Időzítés

A fenti verzió még játszhatatlan, ugyanis alig, hogy lenyomjuk a balra vagy jobbra nyilat, az űrhajót jelképező téglalap egyből a szélére ugrik, nem lehet finomhangolni. Az időzítéssel orvosoljuk ezt a problémát.

A főciklus előtt hozzunk létre egy órát a következőképpen:

óra = pygame.time.Clock()

A főciklus végére pedig írjuk be ezt:

    óra.tick(50)

A paraméter azt mondja meg, hogy másodpercenként hányszor fusson le a belső mag. Az 50 tehát azt jelenti, hogy ötvenszer fut le, azaz a két századmásodpercenként. Ez azt is jelenti, hogy úgy kell megvalósítani a programot, hogy ennyi idő alatt lefusson minden ellenőrzés és minden kirajzolás.

Ezzel a paraméterrel el lehet játszani, és szükség esetén lehet csökkenteni 25-30 ig. Jelentősen ez alá viszont nem mehetünk, mert akkor nagyon "darabos" lesz; ilyenkor a számításon ill. kirajzoláson kell optimalizálnunk.

mozgat.png

Objektumorientált megközelítés

Egy tipikus játékban nemcsak egy, hanem több, adott esetben meglehetősen sok szereplő lehet. A fenti módszerrel nehézkessé válhat a szereplők karbantartása. A pygame lehetővé teszi az objektumorientált megközelítést, melyhez egy külön osztályt is biztosít, pygame.sprite.Sprite néven. A sprite igen gyakori, szokványos elnevezése a szereplőknek; a fogalom más programozási nyelvekben is megjelenik.

A szereplőket érdemes ebből az osztályból származtatni. Valamint célszerű az alábbi konvenciókat betartani:

  • Hozzunk létre egy surf osztályváltozót, ami az adott szereplőhöz tartozó felszínt tárolja.
  • Külön tároljuk el a vonatkozó téglalapot, rect néven: self.rect = self.surf.get_rect(center = (…, …)). Itt célszerű megadnunk a kezdőpozíciót.
  • Legyen egy update() függvény, melyben aktualizáljuk a tulajdonságait, pl. a pozícióját.

Ezek alapján az űrhajót az következőképpen is megvalósíthatjuk:

class Űrhajó(pygame.sprite.Sprite):
    def __init__(self):
        super(Űrhajó, self).__init__()
        self.surf = pygame.Surface((30, 50))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect(center = (KÉPERNYŐ_SZÉLESSÉG // 2, KÉPERNYŐ_MAGASSÁG - 40))
 
    def update(self, lenyomott_billentyűk):
        if lenyomott_billentyűk[pygame.K_LEFT]:
            self.rect.move_ip(-10, 0)
        if lenyomott_billentyűk[pygame.K_RIGHT]:
            self.rect.move_ip(10, 0)
        if self.rect.left < 0:
            self.rect.left = 0
        elif self.rect.right >= KÉPERNYŐ_SZÉLESSÉG:
             self.rect.right = KÉPERNYŐ_SZÉLESSÉG

A pozíciójának a megváltoztatásához globális változók módosítása helyett a téglalap move_ip() függvényét használhatjuk.

Ez így még csak az osztály; példányosítani is kell:

űrhajó = Űrhajó()

A főprogram vonatkozó része az alábbira módosul:

    lenyomott_billentyűk = pygame.key.get_pressed()
    űrhajó.update(lenyomott_billentyűk)
 
    képernyő.fill((0, 0, 0))
    képernyő.blit(űrhajó.surf, űrhajó.rect)

Ellenség időzített megjelenése

A következő lépésben sárga négyzetek formájában megnyilvánuló ellenséget adunk hozzá. Itt több mindent kell megvalósítani:

  • Az űrhajóhoz hasonlóan létrehozunk egy Ellenség osztályt.
  • Egyszerre több ellenség is lehet.
  • Az ellenség bizonyos időzítéssel a képernyő tetején jelenik meg véletlenszerű pozícióban.
  • Lefelé mozog véletlenszerű irányba.
  • Ha leért, meg kell szüntetni.

Az ellenség kódja az alábbi:

class Ellenség(pygame.sprite.Sprite):
    def __init__(self):
        super(Ellenség, self).__init__()
        self.surf = pygame.Surface((30, 30))
        self.surf.fill((255, 255, 0))
        self.rect = self.surf.get_rect(center = (random.randint(0, KÉPERNYŐ_SZÉLESSÉG), 0))
        self.irány = random.randint(-2, 2)
 
    def update(self):
        self.rect.move_ip(self.irány, 3)
        if self.rect.top > KÉPERNYŐ_MAGASSÁG:
            self.kill()

A véletlenítés a szokásos random belső Python könyvtár segítségével történik. Így a fenti kódban az újdonság csak a self.kill(), ami törölni az adott példányt.

Az ellenségeket időzítve hozzuk létre: másodpercenként egyet. Ehhez meg kell valósítanunk az időzítést. Ezt úgy tudjuk megtenni, hogy ún. felhasználói eseményt, azaz user eventet hozunk létre.:

ELLENSÉGET_HOZZÁAD = pygame.USEREVENT + 1
pygame.time.set_timer(ELLENSÉGET_HOZZÁAD, 1000)

Az alacsonyabb sorszámú események foglaltak; az első, amit a felhasználó használhat, az a pygame.USEREVENT + 1.

A fent látható módon hozhatjuk létre az időzítőt, a set_timer() segítségével. Itt második paraméter az, hogy hány ezredmásodpercenként fusson le. A megközelítés tehát ebben az esetben fordított mint a főprogram időzítése: itt NEM azt adjuk meg, hogy másodpercenként hányszor fusson le, hanem azt, hogy hány ezredmásodpercenként fusson le.

A pygame támogatja a sprite csoportok működését: ha több szereplő nagyon hasonlóan viselkedik, akkor célszerű csoportba szervezni őket, és így kihasználni ezt a lehetőséget. Az ellenségeket tartalmazó sprite csoport létrehozása:

ellenségek = pygame.sprite.Group()

Mivel több minden változik, ezért megadom a teljes esemény ciklust:

while fut:
    for esemény in pygame.event.get():
        if esemény.type == pygame.QUIT:
            fut = False
        if esemény.type == ELLENSÉGET_HOZZÁAD:
            ellenség = Ellenség()
            ellenségek.add(ellenség)
 
    lenyomott_billentyűk = pygame.key.get_pressed()
    űrhajó.update(lenyomott_billentyűk)
    ellenségek.update()
 
    képernyő.fill((0, 0, 0))
    képernyő.blit(űrhajó.surf, űrhajó.rect)
    for ellenség in ellenségek:
        képernyő.blit(ellenség.surf, ellenség.rect)
 
    pygame.display.flip()
    óra.tick(50)

Pár újdonság:

  • if esemény.type == ELLENSÉGET_HOZZÁAD: ezzel azt vizsgáljuk, hogy az utolsó lefutás óta bekövetkezett-e a megadott időzítés.
  • ellenség = Ellenség() és ellenségek.add(ellenség): ellenség példányosítása és az ellenség csoportba helyezése.
  • ellenségek.update(): ez egy olyan művelet, ahol kihasználjuk a sprite csoport szolgáltatását: ahelyett, hogy egyesével hívnánk az update()-et, elég magán a csoporton meghívni.
  • for ellenség in ellenségek és képernyő.blit(ellenség.surf, ellenség.rect): a szokásos módon egyesével kirajzoljuk. (Erre nincs egyszerűsítés.)
négyzet.png

Képek használata

A négyzeteket és téglalapokat egy ponton túl célszerű képekre cserélni a jobb játékélmény érdekében.

Az alábbi két képet használhatjuk űrhajó és ellenség gyanánt. (A képeket a Scratch programból készítettem képernyőkép formájában, majd átméreteztem.)

űrhajó.pngszellem.png

Az űrhajó konstruktorában cseréljük le a self.surf részeket az alábbira:

        self.surf = pygame.image.load('űrhajó.png').convert()
        self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL)

Az ellenség kódjában pedig az alábbira:

        self.surf = pygame.image.load('szellem.png').convert()
        self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL)

Magyarázat:

  • self.surf = pygame.image.load('űrhajó.png').convert(): ezzel töltjük be a képet és adjuk meg azt, hogy hogyan nézzen ki a felszín. A korábbi verzióban egy színnel befestettük, ebben a kép jelenik meg.
  • self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL): az első paraméterrel azt adjuk meg, hogy melyik színt kezelje átlátszóként. A feketét adtuk meg. A pygame.RLEACCEL segítségével gyorsítást tudunk elérni, bár szerepét én sem igazán értem. (Legalábbis azt, hogy ha valóban gyorsít, és nem indokolt a használatának a kerülése, akkor miért nem ez az alapértelmezett.)

Technikai megjegyzés: ha Visual Studio Code-ot használunk és a játékot egy alkönyvtárban fejlesztjük, akkor a gyökérbe kell tennünk a képeket, és nem a forráskód mellé.

képek.png

Ütközésvizsgálat

A játék jelen verziója még egyelőre eléggé élvezhetetlen, ugyanis ha ütközik az űrhajó az ellenséggel, a játék akkor is folytatódik. Valósítsunk meg két dolgot:

  • Ellenőrizzük, hogy ütközik-e az űrhajó a szellemmel.
  • Ha igen, írjuk ki, hogy VÉGE, és érjen véget a játék.

A kérdéses kódrészlet a következő, amit a főciklusa írjunk be, praktikusan közvetlenül az esemény ellenőrzés után:

    if pygame.sprite.spritecollideany(űrhajó, ellenségek):
        vége_felirat = pygame.font.Font('freesansbold.ttf', 80).render('VÉGE', True, (255, 255, 0), (0, 0, 0))
        vége_téglalap = vége_felirat.get_rect(center = (KÉPERNYŐ_SZÉLESSÉG // 2, KÉPERNYŐ_MAGASSÁG // 2))
        vége = True
        űrhajó.kill()
        for ellenség in ellenségek:
            ellenség.kill()

A pygame.sprite.spritecollideany() függvény egy igen erős funkciót valósít meg: ahogy a nevéből is következik, megnézi, hogy van-e ütközés az űrhajó és bármelyik ellenség között. Itt is kihasználjuk azt, hogy az ellenségeket csoportba gyűjtöttük. Ez egy olyan pont, ahol igazán megjelenik a pygame erőssége a natív TK Canvas megoldással szemben: ott egyesével kell ellenőrizni, másrészt "matekozni" kell, ami igen bonyolult lehet akkor, ha azt szeretnénk, hogy a fekete részek érintkezése még ne számítson ütközésnek.

A következő két sor a VÉGE feliratot hozza létre. Ismeretlen komponenseket ez a sor tartalmaz: vége_felirat = pygame.font.Font('freesansbold.ttf', 80).render('VÉGE', True, (255, 255, 0), (0, 0, 0)).

  • A Font paraméterei: maga a font és a betűméret.
  • A render() paraméterei: a szöveg, amit meg szeretnénk jeleníteni; ???; a betű színe; a háttér színe.

A legegyszerűbb ezt feljegyezni és "sormintaként" használni.

Ezzel viszont még csak létrehoztuk a feliratot. Megjeleníteni a megjelenítés kódrészletben érdemes, kicsit átalakítva at:

vége = False
while fut:
    ...
    if pygame.sprite.spritecollideany(űrhajó, ellenségek):
        ...
        vége = True
        ...
 
    képernyő.fill((0, 0, 0))
    if not vége:
        lenyomott_billentyűk = pygame.key.get_pressed()
        űrhajó.update(lenyomott_billentyűk)
        ellenségek.update()
 
        képernyő.blit(űrhajó.surf, űrhajó.rect)
        for ellenség in ellenségek:
            képernyő.blit(ellenség.surf, ellenség.rect)
    else:
        képernyő.blit(vége_felirat, vége_téglalap)
vége.png

Lövés

Ebben a lépésben a lövést valósítjuk meg. A golyót szintén a Scratch játékból készítettem:

golyó.png

Amikor példányosítjuk a golyót, akkor a kezdő pozíciója az, ahol az űrhajó van. Folyamatosan halad fölfelé, és amikor eléri az ablak tetejét, akkor eltűnik. A kód:

class Golyó(pygame.sprite.Sprite):
    def __init__(self):
        super(Golyó, self).__init__()
        self.surf = pygame.image.load('golyó.png').convert()
        self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL)
        self.rect = self.surf.get_rect(center = űrhajó.rect.center)
 
    def update(self):
        self.rect.move_ip(0, -5)
        if self.rect.bottom < 0:
            self.kill()

Golyót akkor hozunk létre, amikor a felhasználó lenyomja a szóközt. Egyszerre több golyó is lehet a képernyőn, így ezeket is célszerű egy csoportba gyűjteni. Sőt, érdemes egy olyan csoportot is létrehozni, ami az összes szereplőt tartalmazza:

szereplők = pygame.sprite.Group()
golyók = pygame.sprite.Group()
 
...
 
szereplők.add(űrhajó)

A főciklusban az alábbiakat módosítjuk:

  • Amikor létrehozunk egy ellenséget, akkor hozzáadjuk a szereplők csoportjához.
  • Az eseményeknél ellenőrizzük, hogy a felhasználó lenyomta-e a szóközt. Ha igen, akkor példányosítunk egy golyót, amit hozzáadunk egyrészt a golyók, másrészt a szereplők csoportjához.
  • Amikor a játék véget ér, elég csak a szereplőkön végiglépkedni, nem kell külön az űrhajón, az ellenségeken és a golyókon.
  • Aktualizálni kell a golyókat is. (Azt a szereplők csoporton ebben a formában nem tudjuk megtenni, mert az űrhajónak van paramétere.)
  • A kirajzolásnál elég a szereplőkön végiglépkedni.

Mivel sok kis változtatás van benne, a főciklust teljes egészében megadom:

while fut:
    for esemény in pygame.event.get():
        if esemény.type == pygame.QUIT:
            fut = False
        if esemény.type == ELLENSÉGET_HOZZÁAD:
            ellenség = Ellenség()
            ellenségek.add(ellenség)
            szereplők.add(ellenség)
        if esemény.type == pygame.KEYDOWN:
            if esemény.key == pygame.K_SPACE:
                golyó = Golyó()
                golyók.add(golyó)
                szereplők.add(golyó)
 
    if pygame.sprite.spritecollideany(űrhajó, ellenségek):
        vége_felirat = pygame.font.Font('freesansbold.ttf', 80).render('VÉGE', True, (255, 255, 0), (0, 0, 0))
        vége_téglalap = vége_felirat.get_rect(center = (KÉPERNYŐ_SZÉLESSÉG // 2, KÉPERNYŐ_MAGASSÁG // 2))
        vége = True
        for szereplő in szereplők:
            szereplő.kill()
 
    képernyő.fill((0, 0, 0))
    if not vége:
        lenyomott_billentyűk = pygame.key.get_pressed()
        űrhajó.update(lenyomott_billentyűk)
        ellenségek.update()
        golyók.update()
        for szereplő in szereplők:
            képernyő.blit(szereplő.surf, szereplő.rect)
    else:
        képernyő.blit(vége_felirat, vége_téglalap)
 
    pygame.display.flip()
    óra.tick(50)
lövés.png

Találat

Az aktuális változatban még nem ellenőrizzük azt, hogy a golyó eltalálta-e az ellenséget. Ebben a lépésben ezt valósítjuk meg, valamint pontszámot is számítunk. A már ismert komponenseket használjuk:

...
pontszám = 0
...
while fut:
    ...
    for ellenség in ellenségek:
        if pygame.sprite.spritecollideany(ellenség, golyók):
            ellenség.kill()
            pontszám += 1
    ...
    if not vége:
        ...
        pontszám_felirat = pygame.font.Font('freesansbold.ttf', 15).render(f'Pontszám: {pontszám}', True, (255, 255, 0), (0, 0, 0))
        pontszám_felirat.set_colorkey((0, 0, 0), pygame.RLEACCEL)
        pontszám_téglalap = pontszám_felirat.get_rect(topleft = (10, 10))
        képernyő.blit(pontszám_felirat, pontszám_téglalap)
pontszám.png

Csillagok

A játék bizonyos értelemben már kész van, bizonyos értelemben viszont sohasem lesz kész. Ebben a részben azt az illúzió valósítjuk meg, hogy az űrhajó valóban repül. Ezt oly módon tesszük, hogy csillagokat rajzolunk az "égre" (azaz a háttérre), amelyek lefelé mozognak. EHhez az alábbiakat kell megvalósítanunk:

  • A csillag szereplőt, ahogy az eddigieket.
  • Az alap képernyőt "teleszórni" csillaggal.
  • A csillagok lefelé haladnak.
  • Ha egy csillag eléri az ablak alját, akkor eltűnik.
  • Egy újabb időzítő segítségével véletlenszerűen megjelenítünk az ablak tetején csillagokat.
  • Technikailag szükség van a csillagok csoportjára is.

A csillag szereplő kódja:

class Csillag(pygame.sprite.Sprite):
    def __init__(self, függőleges):
        super(Csillag, self).__init__()
        self.surf = pygame.Surface((2, 2))
        self.surf.fill((0, 0, 0))
        self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL)
        pygame.draw.circle(self.surf, (255, 255, 255), (1, 1), 1)
        self.rect = self.surf.get_rect(center = (random.randint(0, KÉPERNYŐ_SZÉLESSÉG), függőleges))
 
    def update(self):
        self.rect.move_ip(0, 1)
        if self.rect.top > KÉPERNYŐ_MAGASSÁG:
            self.kill()

A csillag egy kis fehér kör. Kört a pygame.draw.circle(felület, (R, G, B), (x, y), r) függvény segítségével tudunk rajzolni. A konstruktornak itt van egy plusz paramétere: a függőleges koordináta. Ez a játék során végig 0 lesz, de az elején bármi lehet. Az update() a mozgatás valósítja meg.

A csillag szereplők csoportja:

csillagok = pygame.sprite.Group()

Kezdeti csillagok (a főciklus előtt):

for i in range(0, KÉPERNYŐ_MAGASSÁG, 10):
    csillag = Csillag(i)
    csillagok.add(csillag)
    szereplők.add(csillag)

A csillag hozzáadásának időzítője:

CSILLAGOT_HOZZÁAD = pygame.USEREVENT + 2
pygame.time.set_timer(CSILLAGOT_HOZZÁAD, 200)

Az eseménykezelésen belülre kerül az újabb csillagok létrehozása:

        if esemény.type == CSILLAGOT_HOZZÁAD:
            csillag = Csillag(0)
            csillagok.add(csillag)
            szereplők.add(csillag)

Tehát másodpercenként 5 új csillag jelenik meg.

Az update() hívások alá a csillagok aktualizálását is be kell írni:

        csillagok.update()
csillagok.png

Hangok

Ebben a szakaszban megismerkedünk a hangokkal. Két külön témát érintünk:

  • a háttérzenét,
  • azokat a hangokat, amelyek valamilyen esemény hatására következnek be.

A játékban 4 hangra van szükségünk. Az alábbiakat a https://mixkit.co/free-stock-music/ oldalról töltöttem le:

Ahhoz, hogy tudjunk zenét lejátszani, a pygame.mixer.init() sor kell beírni az elejére, a pygame.init() elé. Tehát a program első két utasítása tipikusan az alábbi:

pygame.mixer.init()
pygame.init()

A vége:

pygame.mixer.quit()

Itt viszont nem szükséges a pygame.quit(), mivel a pygame.mixer.quit() kilép.

A háttérzene folyamatos lejátszása:

pygame.mixer.music.load('háttérzene.mp3')
pygame.mixer.music.play(loops=-1)

A háttérzene leállítása a játék végén, a VÉGE felirat megjelenítésekor:

        pygame.mixer.music.stop()

A hangok betöltése a főciklus előtt:

lövés_hang = pygame.mixer.Sound('lövés.wav')
találat_hang = pygame.mixer.Sound('találat.wav')
vége_hang = pygame.mixer.Sound('vége.wav')
találat_hang.set_volume(0.5)

Itt példát láthatunk arra is, hogy hogyan tudjuk megváltoztatni a hangerőt. A találat hangerőssége ugyanis túl nagy ebben az esetben.

A hangokat a neki megfelelő helyen indítsuk el. A lövést amikor a felhasználó lenyomja a szóközt:

                lövés_hang.play()

A találatot, amikor a golyó eltalál egy ellenséget:

            találat_hang.play()

A végét pedig akkor, amikor az űrhajó hozzáér egy ellenséghez:

        vége_hang.play()

A példában nincs eset a hang leállítására; azt a stop() függvény hívásával tudjuk megtenni (a start() helyett).

A végeredmény:

# pip install pygame
import pygame
import random
 
KÉPERNYŐ_SZÉLESSÉG = 480
KÉPERNYŐ_MAGASSÁG = 360
 
class Űrhajó(pygame.sprite.Sprite):
    def __init__(self):
        super(Űrhajó, self).__init__()
        self.surf = pygame.image.load('űrhajó.png').convert()
        self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL)
        self.rect = self.surf.get_rect(center = (KÉPERNYŐ_SZÉLESSÉG // 2, KÉPERNYŐ_MAGASSÁG - 40))
 
    def update(self, lenyomott_billentyűk):
        if lenyomott_billentyűk[pygame.K_LEFT]:
            self.rect.move_ip(-10, 0)
        if lenyomott_billentyűk[pygame.K_RIGHT]:
            self.rect.move_ip(10, 0)
        if self.rect.left < 0:
            self.rect.left = 0
        elif self.rect.right >= KÉPERNYŐ_SZÉLESSÉG:
            self.rect.right = KÉPERNYŐ_SZÉLESSÉG
 
class Ellenség(pygame.sprite.Sprite):
    def __init__(self):
        super(Ellenség, self).__init__()
        self.surf = pygame.image.load('szellem.png').convert()
        self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL)
        self.rect = self.surf.get_rect(center = (random.randint(0, KÉPERNYŐ_SZÉLESSÉG), 0))
        self.irány = random.randint(-2, 2)
 
    def update(self):
        self.rect.move_ip(self.irány, 3)
        if self.rect.top > KÉPERNYŐ_MAGASSÁG:
            self.kill()
 
class Golyó(pygame.sprite.Sprite):
    def __init__(self):
        super(Golyó, self).__init__()
        self.surf = pygame.image.load('golyó.png').convert()
        self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL)
        self.rect = self.surf.get_rect(center = űrhajó.rect.center)
 
    def update(self):
        self.rect.move_ip(0, -5)
        if self.rect.top < 0:
            self.kill()
 
class Csillag(pygame.sprite.Sprite):
    def __init__(self, függőleges):
        super(Csillag, self).__init__()
        self.surf = pygame.Surface((2, 2))
        self.surf.fill((0, 0, 0))
        self.surf.set_colorkey((0, 0, 0), pygame.RLEACCEL)
        pygame.draw.circle(self.surf, (255, 255, 255), (1, 1), 1)
        self.rect = self.surf.get_rect(center = (random.randint(0, KÉPERNYŐ_SZÉLESSÉG), függőleges))
 
    def update(self):
        self.rect.move_ip(0, 1)
        if self.rect.top > KÉPERNYŐ_MAGASSÁG:
            self.kill()
 
pygame.mixer.init()
pygame.init()
képernyő = pygame.display.set_mode((KÉPERNYŐ_SZÉLESSÉG, KÉPERNYŐ_MAGASSÁG))
pygame.display.set_caption('Űrhajós játék')
 
szereplők = pygame.sprite.Group()
ellenségek = pygame.sprite.Group()
golyók = pygame.sprite.Group()
csillagok = pygame.sprite.Group()
 
űrhajó = Űrhajó()
szereplők.add(űrhajó)
 
ELLENSÉGET_HOZZÁAD = pygame.USEREVENT + 1
pygame.time.set_timer(ELLENSÉGET_HOZZÁAD, 1000)
 
for i in range(0, KÉPERNYŐ_MAGASSÁG, 10):
    csillag = Csillag(i)
    csillagok.add(csillag)
    szereplők.add(csillag)
CSILLAGOT_HOZZÁAD = pygame.USEREVENT + 2
pygame.time.set_timer(CSILLAGOT_HOZZÁAD, 200)
 
pygame.mixer.music.load('háttérzene.mp3')
pygame.mixer.music.play(loops=-1)
 
# https://mixkit.co/free-stock-music/
lövés_hang = pygame.mixer.Sound('lövés.wav')
találat_hang = pygame.mixer.Sound('találat.wav')
vége_hang = pygame.mixer.Sound('vége.wav')
találat_hang.set_volume(0.5)
 
pontszám = 0
 
óra = pygame.time.Clock()
fut = True
vége = False
while fut:
    for esemény in pygame.event.get():
        if esemény.type == pygame.QUIT:
            fut = False
        if esemény.type == ELLENSÉGET_HOZZÁAD:
            ellenség = Ellenség()
            ellenségek.add(ellenség)
            szereplők.add(ellenség)
        if esemény.type == CSILLAGOT_HOZZÁAD:
            csillag = Csillag(0)
            csillagok.add(csillag)
            szereplők.add(csillag)
        if esemény.type == pygame.KEYDOWN:
            if esemény.key == pygame.K_SPACE:
                golyó = Golyó()
                golyók.add(golyó)
                szereplők.add(golyó)
                lövés_hang.play()
 
    if pygame.sprite.spritecollideany(űrhajó, ellenségek):
        vége_felirat = pygame.font.Font('freesansbold.ttf', 80).render('VÉGE', True, (255, 255, 0), (0, 0, 0))
        vége_téglalap = vége_felirat.get_rect(center = (KÉPERNYŐ_SZÉLESSÉG // 2, KÉPERNYŐ_MAGASSÁG // 2))
        vége = True
        vége_hang.play()
        for szereplő in szereplők:
            szereplő.kill()
 
    for ellenség in ellenségek:
        if pygame.sprite.spritecollideany(ellenség, golyók):
            ellenség.kill()
            pontszám += 1
            találat_hang.play()
 
    képernyő.fill((0, 0, 0))
    if not vége:
        lenyomott_billentyűk = pygame.key.get_pressed()
        űrhajó.update(lenyomott_billentyűk)
        ellenségek.update()
        golyók.update()
        csillagok.update()
        for szereplő in szereplők:
            képernyő.blit(szereplő.surf, szereplő.rect)
        pontszám_felirat = pygame.font.Font('freesansbold.ttf', 15).render(f'Pontszám: {pontszám}', True, (255, 255, 0), (0, 0, 0))
        pontszám_felirat.set_colorkey((0, 0, 0), pygame.RLEACCEL)
        pontszám_téglalap = pontszám_felirat.get_rect(topleft = (10, 10))
        képernyő.blit(pontszám_felirat, pontszám_téglalap)
    else:
        képernyő.blit(vége_felirat, vége_téglalap)
        pygame.mixer.music.stop()
 
    pygame.display.flip()
    óra.tick(50)
 
pygame.mixer.quit()

A szükséges fájlok:

Továbbiak

A játékot természetesen lehetne tovább fejleszteni. Az alábbi pár ötlet megvalósítása megtöbbszörözné a forráskódot:

  • Legyen több élet.
  • A játék végén lehessen újraindítani.
  • A játék nehezedjen: pl. jöjjenek gyorsabban az ellenségek, legyenek egyre többen stb.
  • Az ellenség lőhessen vissza.
  • A rekordot mentsük fájlba, névvel és időponttal együtt, ami jelenjen meg a játék során.
  • Ne legyen elég csak elengedi az ellenséget. Ha nem lőjük le, az járjon mínusz ponttal.
  • Jelenjenek egyéb dolgok is, pl. üzemanyag, ami a mozgással fogy, és időnként fel kelljen venni ahhoz, hogy folytatódhasson a játék.

Csak a fantáziánk szab határt a lehetőségeknek.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License