Játékok készítése a pygame keretrendszerrel - 2. rész

Az előző bevezető rész után térjünk át most sokkal gyakorlatiasabb vizekre. Ismerkedjünk meg mélyebben a pygame.Surface objektumával.

Ahogy említettem már, a Surface egy olyan objektum, amely egy felületet reprezentál, amire rajzolhatunk. De nem csak rajzolhatunk rájuk, hanem betölthetünk képállományokat "beléjük", ezzel a játék egyes elemeit nem vagyunk kénytelenek a pygame nehézkes és soványka rajzolóképességeivel előállítani, hanem használhatunk komoly grafikai szoftvereket ezek elkészítésére.

Javaslom mindenkinek az Inkscape használatát, mert vektoros (emellett ópenszósz és ingyenes), így ha később változtatni akarunk a felbontáson, egyszerűen más mérettel exportáljuk ki a rajzainkat.

Fejlesszül tovább pattogó labdánkat, és csináljunk egy jobban kinéző labdát!

Ehhez csináltam egy strandlabdaszerűséget négy fázissal, hogy úgy nézzen ki mintha forogna pattogás közben. Elég gagyi lett mert csak összeférceltem, de arra, amit mutatni szeretnék, tökéletesen megfelel.



Nézzük a feladatot!

Azt szeretnénk csinálni, hogy a forgó strandlabdánk egy szürke "kockás" háttér felett mozogva pattogjon nekünk az ablakunkban. Hogyan tudjuk ezt elérni?

Nos, kell egy háttérkép, aztán egy labda, aminek a képét cserélgetjük, ezzel keltve a forgó hatást. A mozgatás hasonlóan megy az előző részben megismertekhez.

Hogy fog működni a programunk?

Csinálunk egy háttérkép, és egy labda objektumot. A főciklusban kirajzoljuk a háttérképet, elvégezzük a labda mozgatását, frissítjük a labda aktuális fázisát, és kirajzoljuk. Megnézzük történt-e lekezelendő esemény (pl. ablak bezárás) amit lekezelünk, majd várakozunk a megadott fps betartásához, és kezdjük elölről.
Nem hangzik túl bonyolultnak ugye? Hogy mégis az legyen, objektumorientáltan készítjük el ;)

A főciklusunk a következően fog kinézni:

# fő ciklus
# ismétlődik amíg ki nem lépünk
while True:
    render() # aktuális képkocka létrehozása

    # ki akart lépni a felhasználó?
    if kilep():
        pygame.quit()
        sys.exit()

    # kirakjuk az ablak tartalmát a képernyőre
    pygame.display.flip()

    # szabályozzuk a futási sebességet
    clock.tick(fps)

- vagyis -

kirajzolunk egy képkockát, megnézzük az előző képkocka óta bekövetkezett eseményeket - jelen esetben csak a kilépési szándékot vizsgáljuk -, frissítjük a képernyőt, és szabályozzuk a futási sebességet, majd kezdjük ezeket előlről.

Eddig nem bonyolult. Most nézzük meg, hogyan hozzuk létre a képkockát:

def render():
    screen.blit(hatter,hatter.rect)
    labdak.update()
    labdak.draw(screen)

Szavakban: rakjuk a képernyőre a hátteret, frissítsük a labdákat, és rajzoljuk ki őket.
Hát ez sem tűnik bonyolultnak... De azért álljunk meg egy kicsit. Adós vagyok még a hatter és labdak objektumok bemutatásával, de előtte a blit metódusról ejtünk szót.
A screen ugye nem felejtettük el, hogy egy pygame.Surface, amire rajzolni lehet. A Surface objektumoknak van egy blit metódusa (BLock Image Transfer), aminek segítségével más Surface objektumok képét lehet rájuk rajzolni. Ennek két paramétert kell adni, mit akarunk kirajzolni, és hova. A mit egy másik Surface objektum kell legyen, a hova pedig egy pygame.Rect objektum. A Rect tulajdonképpen egy téglalap, amit megadhatnánk a bal felső sarkának két koordinátájával, és szélességi, magassági adatával is, ám a Rect objektum használatának előnye az, hogy ő a téglalap több adatát menedzseli, amelyeket le lehet kérdezni (pl. középpont, alsó koordináta, jobb alsó sarok koordinátája, stb.). Ezek automatikusan módosulnak, ha a téglalap valamelyik adata módosításra kerül.
A labdak objektum itt egy pygame.sprite.Group. Aki nem találkozott még játékprogramok készítésével, a sprite szó nem mond semmit. A szó maga manót jelent, de az ősjátékok kialakulásakor ez az alapvető mozgatható grafikai elemet takarta, aminek van grafikája, fázisai, mérete, pozíciója, stb. A group csoportot jelent, vagyis a labdak egy sprite-okból álló csoport. A labdak.update() a csoport minden tagját frissíti (mozgatja, és ha kell fázist módosít), majd a labdak.draw(screen) kirajzolja a csoport minden elemét a screen felületünkre.

A két objektumunk bemutatása előtt még egy kitérőt teszünk, megnézzük, hogy a főciklus indítása előtt miket állítottunk be.

# ablak megjelenítése és a játékadatok beállítás
SCREEN_SIZE = (800,600) # ablak mérete
screen = pygame.display.set_mode(SCREEN_SIZE) # ablak "surface"

hatter = Hatter(SCREEN_SIZE,50,((192,192,192),(255,255,255)))
labdak = pygame.sprite.Group()
labdafazisok = [pygame.image.load('labda1.png').convert_alpha(),pygame.image.load('labda2.png').convert_alpha(),pygame.image.load('labda3.png').convert_alpha(),pygame.image.load('labda4.png').convert_alpha()]
labda = Labda(labdafazisok,[6,5],SCREEN_SIZE,5,group = labdak)
fps = 30

Megalkotjuk ugye a screen felületünket, elkészítjük a hatter objektumot a Hatter nevű classunkból, definiáljuk a labdak sprite csoportot.
Betöltjük a labdafázisokat a labdafazisok listába. Ehhez a pygame.image.load-ot használjuk, ami egy képfájlt betöltve Surface objektumokat hoz létre. Ahhoz, hogy a blit utasítások hatékonyak legyenek a sebességet tekintve, érdemes a Surface objektumok képadatait egy belső formátumra konvertálni, erre szolgál a Surface objektumok convert() és convert_alpha() metódusa. Ez utóbbit akkor használjuk, ha kép átlátszó területeket alkalmaz. Ha ezt a konvertálást nem végezzük el, a blit metódusok menet közben elvégzik minden alkalommal automatikusan, de ez természetesen hatással van a kész játék futási sebességére és processzoréhségére.
Elkészítjük a labda objektumot a Labda classunkból, és beállítjuk az fps változót, amit a főciklus időzítésénél használunk.

Elérkeztünk classaink bemutatásához, nézzük tehát, hogyan is néznek ki.

A Hatter class:

class Hatter(pygame.Surface):
    def __init__(self,size,msize,colors):
        super(Hatter,self).__init__(size)
        self.rect = self.get_rect()
        xq = size[0] / msize
        yq = size[1] / msize
        colorindex = 0
        for y in range(yq):
            firstcolor = colorindex
            for x in range(xq):
                pygame.draw.rect(self,colors[colorindex],(x*msize,y*msize,msize,msize),0)
                colorindex = 0 if colorindex == 1 else 1
            colorindex = 0 if firstcolor == 1 else 1

Látható, hogy egy származtatott Surface osztályról van szó. A lényege, hogy a megadott méretek és színek alapján egy "kockás" felületet hoz létre (tessék játszani a paraméterekkel bátran...).


A Labda class:

class Labda(pygame.sprite.Sprite):
    def __init__(self,imagelist,vector,screen_size,animspeed = 15, startpoint = [0,0],group = None):
        super(Labda,self).__init__()
        self.imagelist = imagelist
        self.image = self.imagelist[0]
        self.rect = self.image.get_rect()
        self.rect.topleft = startpoint
        self.index = 0
        self.phases = len(self.imagelist) - 1
        self.vector = vector
        width = self.rect.width
        height = self.rect.height
        self.xlimits = (0,screen_size[0] - width)
        self.ylimits = (0,screen_size[1] - height)
        self.speed = animspeed
        self.counter = 0
        if group != None:
            self.add((group))

    def update(self):
        x = self.rect.topleft[0] + self.vector[0]
        y = self.rect.topleft[1] + self.vector[1]
        if x < self.xlimits[0]:
            x = self.xlimits[0]
            self.vector[0] = -self.vector[0]
        if x > self.xlimits[1]:
            x = self.xlimits[1]
            self.vector[0] = -self.vector[0]
        if y < self.ylimits[0]:
            y = self.ylimits[0]
            self.vector[1] = -self.vector[1]
        if y > self.ylimits[1]:
            y = self.ylimits[1]
            self.vector[1] = -self.vector[1]
        self.rect.topleft = x, y
        self.counter += 1
        if self.counter > self.speed:
            self.counter = 0
            self.index += 1
            if self.index > self.phases:
                self.index = 0
            self.image = self.imagelist[self.index]

Labda osztályunk egy olyan származtatott pygame.sprite.Sprite osztály, amely tárolja a saját animációs fázisait, a labda sebességét, az animáció sebességét, a pozícióját, és már az objektum létrehozásakor hozzáadható egy létező sprite-group-hoz.
Az update() metódusa a beállított sebességadatok alapján megváltoztatja a labda pozícióját, az animációsebesség alapján pedig a fázisát. Tessék elemezni, ha valami nem érthető akkor meg kérdezni!

A kész alkotásról egy képernyőkép:


És itt a teljes forráskód egyben:

#!/usr/bin/env python
# -*- coding:utf8 -*-

import sys,pygame
from pygame.locals import *

# pygame inicializálása
pygame.init()

class Hatter(pygame.Surface):
    def __init__(self,size,msize,colors):
        super(Hatter,self).__init__(size)
        self.rect = self.get_rect()
        xq = size[0] / msize
        yq = size[1] / msize
        colorindex = 0
        for y in range(yq):
            firstcolor = colorindex
            for x in range(xq):
                pygame.draw.rect(self,colors[colorindex],(x*msize,y*msize,msize,msize),0)
                colorindex = 0 if colorindex == 1 else 1
            colorindex = 0 if firstcolor == 1 else 1

class Labda(pygame.sprite.Sprite):
    def __init__(self,imagelist,vector,screen_size,animspeed = 15, startpoint = [0,0],group = None):
        super(Labda,self).__init__()
        self.imagelist = imagelist
        self.image = self.imagelist[0]
        self.rect = self.image.get_rect()
        self.rect.topleft = startpoint
        self.index = 0
        self.phases = len(self.imagelist) - 1
        self.vector = vector
        width = self.rect.width
        height = self.rect.height
        self.xlimits = (0,screen_size[0] - width)
        self.ylimits = (0,screen_size[1] - height)
        self.speed = animspeed
        self.counter = 0
        if group != None:
            self.add((group))

    def update(self):
        x = self.rect.topleft[0] + self.vector[0]
        y = self.rect.topleft[1] + self.vector[1]
        if x < self.xlimits[0]:
            x = self.xlimits[0]
            self.vector[0] = -self.vector[0]
        if x > self.xlimits[1]:
            x = self.xlimits[1]
            self.vector[0] = -self.vector[0]
        if y < self.ylimits[0]:
            y = self.ylimits[0]
            self.vector[1] = -self.vector[1]
        if y > self.ylimits[1]:
            y = self.ylimits[1]
            self.vector[1] = -self.vector[1]
        self.rect.topleft = x, y
        self.counter += 1
        if self.counter > self.speed:
            self.counter = 0
            self.index += 1
            if self.index > self.phases:
                self.index = 0
            self.image = self.imagelist[self.index]

# ablak megjelenítése és a játékadatok beállítás
SCREEN_SIZE = (800,600) # ablak mérete
screen = pygame.display.set_mode(SCREEN_SIZE) # ablak "surface"

hatter = Hatter(SCREEN_SIZE,50,((192,192,192),(255,255,255)))
labdak = pygame.sprite.Group()
labdafazisok = [pygame.image.load('labda1.png').convert_alpha(),pygame.image.load('labda2.png').convert_alpha(),pygame.image.load('labda3.png').convert_alpha(),pygame.image.load('labda4.png').convert_alpha()]
labda = Labda(labdafazisok,[6,5],SCREEN_SIZE,5,group = labdak)
fps = 30

def kilep():
    """kilep():
    Megnézi, hogy megnyomták-e az ESC gombot
    vagy megpróbálták bezárni az ablakot"""
    for event in pygame.event.get():
        if event.type==QUIT:
            return True
        elif (event.type==KEYDOWN and event.key==K_ESCAPE):
            return True
    return False

def render():
    screen.blit(hatter,hatter.rect)
    labdak.update()
    labdak.draw(screen)

# ezzel az objektummal tudjuk szabályozni a játék sebességét
clock = pygame.time.Clock()

# fő ciklus
# ismétlődik amíg ki nem lépünk
while True:
    render() # aktuális képkocka létrehozása

    # ki akart lépni a felhasználó?
    if kilep():
        pygame.quit()
        sys.exit()

    # kirakjuk az ablak tartalmát a képernyőre
    pygame.display.flip()

    # szabályozzuk a futási sebességet
    clock.tick(fps)

Vajon mi történik, ha a labda = ... sor alá beteszünk még egy sort ezzel a tartalommal?

labda2 = Labda(labdafazisok,[4,9],SCREEN_SIZE,4,group = labdak)

Tessék kipróbálni!

2 megjegyzés:

  1. A kövektező résznél van problémám:
    xq = size[0] / msize
    yq = size[1] / msize
    colorindex = 0
    for y in range(yq):
    firstcolor = colorindex

    A kövektező a hibaüzenet:
    TypeError: 'float' object cannot be interpreted as an integer

    Megoldás
    xq = size[0] // msize
    yq = size[1] // msize
    colorindex = 0
    for y in range(yq):
    firstcolor = colorindex

    VálaszTörlés
  2. Jogos, valóban, python3-tól ;)
    python 2.7-nél a '/' int-et ad vissza, ha az osztandó is int
    Python 2.7.12 (default, Nov 19 2016, 06:48:10)
    [GCC 5.4.0 20160609] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> 800.0/15
    53.333333333333336
    >>> 800/15
    53

    VálaszTörlés