Tutoriály Pygame
Pomoc! Jak mám pohnout s tím obrázkem?

autor: Pete Shinners pete@shinners.org
překlad: Pavel Kosina pkosina@seznam.cz

Revize 1.2, 20. srpna 2002


Krušné časy zažívá spousta lidí, kteří jsou nováčky v programování a v grafice, když chtějí pohnout s obrázkem na obrazovce. Bez porozumění celkovému konceptu to může být dost neprůhledné. Nejste první člověk, který se zde zastavil, vynasnažím se co možná nejlépe podat věci krok za krokem. Na konci si dokonce zkusíme metody, které zefektivní naši animaci.

Nebudeme učit programovat v Pythonu, jen představíme něco ze základů pygame.

Jen pixely na obrazovce

V pygame je display Plocha (Surface). To je v základě obrázek, který je viditelný na obrazovce. Obrázek je sestaven z pixelů. Hlavním způsobem, jak změnit tyto pixely, je volání funkce blit(). Ta kopíruje pixely z jednoho obrázku na druhý.

To je první na pochopení. Když vykreslíte (blit) něco na obrazovku, jednoduše měníte barvy pixelů na obrazovce. Pixely se nepřidávají ani neposouvají, jen měníme barvu těch pixelů, které jsou na obrazovce. Vykreslené obrázky na obrazovce jsou také v pygame Plochy, ale nejsou žádným způsobem propojeny s Plochou displeje. Když se vykreslují na obrazovce, kopírují se na display, ale vy pořád máte jedinečnou kopii originálu.

S tímto stručným popisem snad již porozumíte, co je potřeba k "posunu" obrázku. Ve skutečnosti s ničím neposunujeme. Jednoduše vykreslíme obrázek v nové pozici. Předtím, než to uděláme, musíme "smazat" ten starý. Jinak bude obrázek vidět na obrazovce na dvou místech. Iluzi pohybu dosahujeme rychlým výmazem obrázku a jeho překreslením na nové místo.

Ve zbytku tohoto tutoriálu rozdělíme tento proces na jednodušší kroky. Dokonce si ozřejmíme nejlepší způsoby jak dosáhnout pohybu několika obrázků po obrazovce. Asi máte již spousty otázek. Jako třeba: jak "vymazat" obrázek předtím, než ho namalujeme v nové pozici? Možná jste stále totálně mimo? Doufejme, že zbytek tohoto tutoriálu vám věci přiblíží více.

Udělejme krok zpátky

Je koncept pixelů a obrázků vám stále trochu cizí? Mám pro vás dobrou zprávu, několik příštích odstavců se budeme zabývat kódem, který bude dělat vše co potřebujeme, jen nebude používat pixely. Vytvoříme malý pythonýrský seznam ze 6 čísel a budeme si představovat, že znázorňují nějakou fantastickou grafiku na obrazovce. Asi bude pro dosti překvapující, jak přesně to bude znázorňovat, co budeme dělat později se skutečnou grafikou.

Začněme vytvořením našeho seznamu obrazovky a vyplňme ho úžasnou krajinou jedniček a dvojek.
>>> obrazovka = [1, 1, 2, 2, 2, 1]
>>> print obrazovka
[1, 1, 2, 2, 2, 1]
Tím jsme si vytvořili pozadí. Nebude moc vzrušující, dokud nenakreslíme na obrazovku také hráče. Vytvoříme mocného hrdinu, který bude vypadat jako číslo 8. Strčme ho někam doprostřed mapy a podívejme se, jak to bude vypadat.
>>> obrazovka[3] = 8
>>> print obrazovka
[1, 1, 2, 8, 2, 1]

Teď jsme tedy tak daleko, jako kdybychom skočili přímo na nějaké grafické programování s pygame. Máte na obrazovce vcelku slušný materiál, ale nelze s ním nikam hýbat. Možná teď, když nám obrazovku představuje seznam čísel, to bude jednodušší pochopit?

Hýbeme s hrdinou

Před tím, než s ním začneme hýbat, musíme si nějak pamatovat pozici, na které právě je. Udělejme to tentokrát nyní úředničtěji.
>>> poziceHrdiny = 3
>>> obrazovka[poziceHrdiny] = 8
>>> print obrazovka
[1, 1, 2, 8, 2, 1]
Nyní bude snadné ho posunout na nové místo. Jednoduše změníme hodnotu poziceHrdiny a vykreslíme ho znovu na obrazovku.
>>> poziceHrdiny = poziceHrdiny - 1
>>> obrazovka[poziceHrdiny] = 8
>>> print obrazovka
[1, 1, 8, 8, 2, 1]
Ooops. Teď máme hrdiny dva. Jednoho na staré pozici a jednoho na nové. To je přesně ten důvod, proč je třeba nejdříve vymazat hrdinu z jeho staré pozice. K tomu potřebujeme znát, co na této pozici bylo předtím, než jsme ho tam vykreslili. To znamená, že si musíme ještě pamatovat hodnoty obrazovky před vykreslením hrdiny. Je několik cest, jak toho dosáhnout, ale snad nejednodušší cesta je schovat si odděleně kopii pozadí obrazovky. To ale znamená, že musíme v naší malé hře udělat malé úpravy.

Vytváříme mapu

Chceme tedy vytvořit oddělený seznam s naším pozadím. Vytvoříme pozadí, které bude vypadat jako vypadala naše originální obrazovka s jedničkami a dvojkami. Pak zkopírujeme každou položku z pozadí na obrazovku. Poté již můžeme vesele vykreslit na obrazovku opět našeho hrdinu.
>>> pozadi = [1, 1, 2, 2, 2, 1]
>>> obrazovka = [0]*6 # nová prázdná obrazovka
>>> for i in range(6):
... obrazovka[i] = pozadi[i]
>>> print obrazovka
[1, 1, 2, 2, 2, 1]
>>> poziceHrdiny = 3
>>> obrazovka[poziceHrdiny] = 8
>>>> print obrazovka
[1, 1, 2, 8, 2, 1]

Možná to vypadá na spoustu nadbytečné práce a přitom nejsme dále než jsme byli předtím, když jsme se ho snažili posouvat. Tentokrát ale máme extra informaci, kterou potřebujeme na řádný pohyb.

Hýbeme s hrdinou (podruhé)

Nyní bude snadnější pohnout s hrdinou. Nejdříve ho vymažeme ze staré pozice tak, že na jeho místo nakopírujeme správnou hodnotu pozadí, které tam bylo před ním. Potom ho vykreslíme na nové místo.
>>> print obrazovka
[1, 1, 2, 8, 2, 1]
>>> obrazovka[poziceHrdiny] = pozadi[poziceHrdiny]
>>> poziceHrdiny = poziceHrdiny - 1
>>> obrazovka[poziceHrdiny] = 8
>>> print obrazovka
[1, 1, 8, 2, 2, 1]
A je to. Hrdina se posunul o jedno místo doleva. Tento kód můžeme opakovaně použít při posunu doleva.
>>> obrazovka[poziceHrdiny] = pozadi[poziceHrdiny]
>>> poziceHrdiny = poziceHrdiny - 1
>>> obrazovka[poziceHrdiny] = 8
>>> print obrazovka
[1, 8, 2, 2, 2, 1]
Výborně! Není to, jak byste asi rádi, hladká animace, ale s několika malými změnami to bude fungovat stejně s grafikou na obrazovce.

Definice: "blit" (vykreslení)

V tomto odstavci přeměníme používání seznamů v našem programu na použití skutečné grafiky na obrazovce. Když zobrazujeme grafiku, používáme často termín blit (vykreslení). Tento běžný termín vám asi není znám, pokud jste nováčky v grafickém prostředí.

BLIT: V podstatě to znamená nakopírovat grafiku z jednoho obrázku na druhý. Formálnější definice říká, že se jedná o kopii matice dat zdroje do bitmapové matice cíle. Můžete si představit vykreslení prostě jako "převod" pixelů. Podobá se to dosti nastavování hodnot v našem seznamu obrazovky ve výše uvedených příkladech. Vykreslení nastavuje barvy pixelů v našem obrázku.

Ostatní grafické knihovny používají slovo bitblt nebo jen blt, ale mluví stále o stejné věci. V zásadě se jedná o kopírování paměti z jednoho místa na druhé. Je to vlastně trochu složitější než pouhé přímé nakopírování paměti, protože je třeba přitom ošetřit věci jako formát pixelu, ořezávání nebo ....scanline pitches. Dokonalejší vykreslovače mohou také ošetřovat věci jako průhlednost a další specielní efekty.

Přecházíme ze seznamu na obrazovku

Převedení kódu z výše uvedených příkladů do pygame je velmi přímočaré. Předpokládejme, že jsme nahráli pěkně slušnou grafiku a pojmenovali jsme ji "teren1", "teren2", atd a "hrdina". Kde jsme předtím přiřazovali čísla do seznamu, nyní budeme vykreslovat grafiku na obrazovku. Další velkou změnou je, že místo použití pozice jako jediného indexu (od 0 do 5), nyní použijeme dvourozměrné souřadnice. Předpokládejme, že každá grafika (obrázek) v naší hře je 10 pixelů široká.
>>> pozadi = [teren1, teren1, teren2, teren2, teren2, teren1]
>>> obrazovka = vytvorObrazovku()
>>> for i in range(6):
... obrazovka.blit(pozadi[i], (i*10, 0)) # aby byly umístěny vedle sebe a nepřekrývaly se
>>> poziceHrdiny = 3
>>> obrazovka.blit(obrazekHrdiny, (poziceHrdiny*10, 0))
Hmm, kód by měl mít známou strukturu a což je důležitější, tento kód by měl dávat trochu smysl. Snad moje přirovnání s nastavením hodnot v seznamu ukazuje podobnost s nastavením pixelů na obrazovce (pomocí blit). Jediná část, která vypadá skutečně jinak a která zatím není vysvětlena dostatečně, je pozice hrdiny na souřadnicích obrazovky. Zatím nám musí stačit nevysvětlené (poziceHrdiny*10,0), ale brzy to napravíme. Nyní pohněme s hrdinou v prostoru. Tento kód by neměl být překvapením.
>>> obrazovka.blit(pozadi[poziceHrdiny], (poziceHrdiny*10, 0))
>>> poziceHrdiny = poziceHrdiny - 1
>>> obrazovka.blit(obrazekHrdiny, (poziceHrdiny*10, 0))
A je to. S tímto kouskem kódu dokážeme zobrazit jednoduché pozadí s hrdinou. Pak řádně posuneme hrdinu doleva o jedno místo. Kam se vydáme odsud? Pro někoho je stále tento kód trochu hloupý. Budeme se snažit zaprvé najít čistější způsob znázorňování pozadí a pozice hráče. Pak snad trochu hladší, skutečnější animace.

Souřadnice obrazovky

Chceme-li umístit objekt na obrazovku, potřebujeme říci metodě blit(), kam ten obrázek dát. V pygame vždy předáváme pozici jako souřadnice (x,y). To představuje počet pixelů doprava a počet pixelů dolů a na tomto místě bude umístěn obrázek. Levý horní roh Plochy má souřadnici (0,0) . Posunutím trochu směrem doprava se dostaneme na (10,0) a dalším posunutím směrem dolů jsme na souřadnici (10,10). Když vykreslujeme pomocí blit, tyto souřadnice představují, kde by měl být umístěn levý horní roh obrázku.

Pygame přichází s pohodlnou schránkou pro tyto souřadnice - je to Rect (Obdélník). Rect v zásadě představuje obdélníkovou oblast na těchto souřadnicích. Má levý horní roh a velikost. Rect přichází s mnoha pohodlnými metodami, které pomáhají při jejich posunu a umísťování. V našem dalším příkladu budeme používat místo souřadnic našich objektů Obdélníky.

Je také známo, že mnoho funkcí v pygame očekává jako argumenty Obdélníky. Všechny tyto funkce také přijímají obyčejné tuple ze 4 položek (vlevo, nahoru, šířka, výška). Není nutné vždy používat objekty Obdélníků, ale povětšinou to bude výhodné. I funkce blit() umí přijmout jako argument Obdélník - jednoduše použije levý horní roh jako skutečnou pozici..

Měníme pozadí

Ve všech našich předchozích odstavcích jsme ukládali pozadí jako seznam různých typů terénů. To je dobrá cesta, když chceme udělat "dlaždicovou" (tile-based) hru, ale my chceme hladké posuny. Abychom to trochu zjednodušili, změníme pozadí na jediný obrázek, který bude vyplňovat celou obrazovku. Budeme-li chtít "smazat" u tohoto typu pozadí naše objekty (předtím, než je překreslíme), musíme jen vykreslit část původního pozadí na obrazovku.

Předáním dobrovolného třetího argumentu funkci blit žádáme, aby se vykreslila jen část našeho zdrojového obrázku. Níže uvidíte konkrétní použití při mazání obrázku hrdiny.

Všimněte si také, že když nyní budeme hotovi s vykreslováním na obrazovku, že zavoláme pygame.display.update(), které nám následně ukáže vše na skutečné obrazovce.

Hladký pohyb

Chceme-li, aby něco vypadalo, že se to hýbe hladce, musíme s tím v jednom okamžiku pohnout jen o několik pixelů, Zde je kód, který hladce posouvá objekt přes obrazovku. Je to snad jednoduché, když si uvědomíme co již známe. Již nejde o experimentování na příkazovém řádku, ale o celý program, takže vypustíme >>> na začátcích řádků.

obrazovka = vytvorObrazovku()
hrdina = nahrajObrazekHrdiny()
pozadi = nahrajObrazekPozadi()

obrazovka.blit(pozadi, (0, 0)) #vykresli pozadí
pozice = hrdina.get_rect()
obrazovka.blit(hrdina, pozice) #vykresli hrdinu
pygame.display.update() #vše to ukaž

for x in range(100): #animace 100 snímků
obrazovka.blit(pozadi, pozice, pozice) #vymazat
pozice = pozice.move(2, 0) #posuň hrdinu
obrazovka.blit(hrdina, pozice) #vykresli nového hrdinu
pygame.display.update() #vše to ukaž
pygame.time.delay(100) #zastav program na 1/10 sekundy
Tak asi tak. Je to celý kód, který potřebujeme, když chceme hladce posouvat objekt po obrazovce. Další výhoda tohoto způsobu vytvoření pozadí je, že obrázek hrdiny může mít průhlednost nebo vystřižené oblasti a přesto se bude na obrazovce vykreslovat správně (bonus zdarma).

Na konec cyklu jsme také připojili volání pygame.time.delay(). To trochu zpomalí náš program, jinak by mohl být také tak rychlý, že bychom nic neviděli.

A co dál?

To bychom tedy měli. Snad tento článek splnil vše, co slíbil. Nicméně v tomto okamžiku náš kód ještě není hotov, aby z něj byla další veleúspěšná hra. Jak uděláme, abychom měli několik pohybujících se objektů? A co jsou ty tajemné funkce jako nahrajObrazekHrdiny()? Také by se nám hodil jednoduchý vstup od uživatele a smyčka s více než 100 snímky. Použijeme příklad, který zde máme a přeložíme ho do objektově orientovaného výtvoru, aby na nás mohla být maminka pyšná.

Tajemné funkce

Podrobné informace o těchto funkcích můžete nalézt v jiných tutoriálech nebo v manuálu. Modul pygame.image má funkci load(),která umí co chceme. Řádky, kde se nahrávají obrázky budou vypadat takto.
hrdina = pygame.image.load('hrdina.jpg').convert()
pozadi = pygame.image.load('pozadi.jpg').convert()
Vidíme, že je to celkem snadné, funkce prostě bere jméno souboru a vrací novou Plochu s nahraným obrázkem. Po nahrátí voláme metodu Plochy convert(). Ta vrací opět novou Plochu z obrázku, nyní však konvertovanou do stejného pixelového formátu jako je dáš display. Naše vykreslování bude tudíž velmi rychlé, protože máme stejné formáty displeje i obrázků. Kdybychom nezkonvertovali, funkce blit() by byla pomalejší, protože při každém volání by musela konvertovat z jednoho typu pixelů do jiného.

Možná jste si také všimli, že jak load() tak convert() vrací nové Plochy. To znamená, že ve skutečnosti na každém řádku vytváříme dvě Plochy. V jiných programovacích jazycích to má za následek paměťovou díru (to není dobrá věc). Python je dostatečně bystrý, že to ošetří a pygame řádně uklidí Plochu, kterou jsme nepoužili.

Další tajemná funkce, kterou jsme viděli výše, je vytvorObrazovku(). Pygame umožňuje snadno vytvořit nové okno pro grafiku. Kód na vytvoření plochy 640x480 je níže. Bez argumentů pygame prostě vybere nejlepší barevnou hloubku a pixelový formát sama.
obrazovka = pygame.display.set_mode((640, 480))

Ošetřujeme vstup

Zoufale potřebujeme změnit hlavní cyklus, abychom mohli kontrolovat vstupy od uživatele (jako třeba když uživatel zavírá okno). Potřebujeme přidat do našeho programu "ošetření událostí". Všechny grafické programy používají toto schéma založené na událostech. Od počítače dostává události jako "klávesa stisknuta" nebo "myš se pohnula". Program pak na tyto události nějak reaguje. Tady je, jak by program měl vypadat. Namísto iterace přes 100 snímků, budeme iterovat dokud uživatel neřekne stop.
while 1:
for udalost in pygame.event.get():
if udalost.type in (QUIT, KEYDOWN):
sys.exit()
posun_a_vykresli_vsechny_objekty()
Ve smyčce budeme procházet navěky a kontrolovat, jestli nastala nějaká událost od uživatele.Opustíme program, pokud uživatel stiskl něco na klávesnici nebo zavřel okno. Po kontrole všech událostí přesuneme naše objekty. Nezapomeneme je předtím smazat. Slova QUIT a KEYDOWN jsou konstanty obsažené v modulu pygame.locals, který nesmíme zapomenou do našeho programu naimportovat.

Pohyb více obrázků

Přichází odstavec, kde skutečně podstatně změníme návrh našeho kódu. Řekněme, že potřebujeme 10 různých obrázků pohybujících se po obrazovce. Pythonýrské třídy jsou dobrým způsobem, jak toho dosáhnout. Vytvoříme třídu, která bude reprezentovat náš herní objekt. Tento objekt bude mít metodu, aby se sám posouval. Těchto objektů můžeme pak vytvořit kolik chceme. Kreslící a posouvací funkce musí pracovat tak, aby se posouvaly jen jednou v jednom snímku (v jednom kroku). Zde je pythonýrský kód pro naši třídu.
class ObjektHry:
def __init__(self, obrazek, vyska, rychlost):
self.rychlost = rychlost
self.obrazek = obrazek
self.pozice = obrazek.get_rect().move(0, vyska)
def posun(self):
self.pozice = self.pozice.move(self.rychlost, 0)
if self.pozice.right > 640:
self.pozice.left = 0
V naší třídě máme dvě funkce. Funkce init vytváří náš objekt. Nastavuje jeho polohu a rychlost. Funkce posun pohne s objektem o jeden krok. Pokud se dostane příliš daleko, vrátí ho zpět doleva.

Spojíme vše dohromady

S naší novou třídou můžeme sestavit celou hru. Bude vypadat takto.

import pygame, sys
from pygame.locals import *
pygame.init()

class ObjektHry:
def __init__(self, obrazek, vyska, rychlost):
self.rychlost = rychlost
self.obrazek = obrazek
self.pozice = obrazek.get_rect().move(0, vyska)
def posun(self):
self.pozice = self.pozice.move(self.rychlost, 0)
if self.pozice.right > 640:
self.pozice.left = 0

obrazovka = pygame.display.set_mode((640, 480))
hrdina = pygame.image.load('hrdina.jpg').convert()
pozadi = pygame.image.load('pozadi.jpg').convert()
obrazovka.blit(pozadi, (0, 0))
objekty = []

for x in range(10):
o = ObjektHry(hrdina, x*40, x)
objekty.append(o)

while 1:
for udalost in pygame.event.get():
if udalost.type in (QUIT, KEYDOWN):
sys.exit()
for o in objekty:
obrazovka.blit(pozadi, o.pozice, o.pozice)
for o in objekty:
o.posun()
obrazovka.blit(o.obrazek, o.pozice)
pygame.display.update()
pygame.time.delay(100)

To je vše. Tímto kódem animujeme 10 objektů na obrazovce. Jediný bod, který snad ještě potřebuje vysvětlení je, proč máme dvě smyčky, kde mažeme a kreslíme všechny objekty. Před vykreslením všech objektů je třeba tyto nejdříve smazat. V našem příkladě by snad nevadilo, kdybychom mazání i kreslení dali do smyčky jediné, ale když se objekty překrývají, jsou dvě smyčky důležité.

Dál již po svých

Takže co bude další na vaší cestě k učení? Nejdříve si pohrajte trochu s tímto příkladem. Přepište kód do editoru a zkuste měnit nejrůznější věci. Hrajte si s ním, pouštějte ho a učte se.

Snad budete chtít pracovat na tom, abyste měli více než jeden typ objektu. Abyste nalezli způsob, jak čistě "smazat" objekt, když už ho nebudete vůbec potřebovat. Také změna volání display.update(), abyste mu mohli předávat seznam oblastí na obrazovce, které se změnily.

Jsou i další tutoriály v pygame, které zahrnují tuto tématiku. Takže když jste ready, pokračujte ve čtení. :-)

Nakonec vás pozývám do pygame mailing listu nebo irc s jakoukoliv otázkou k tomuto tématu. Vždycky tam někdo je, kdo vám podá pomocnou ruku.

A nakonec - hodně zábavy, od toho přeci hry jsou!