|
| 1 | +# Hra typu Had |
| 2 | + |
| 3 | +Dnes to všechno — třídy, grafiku, seznamy a tak dále – |
| 4 | +spojíme dohromady do závěrečného projektu. |
| 5 | +Doufám, že se ti bude líbit! |
| 6 | + |
| 7 | +Naším cílem bude vytvořit klon známé hry [Snake (neboli Had)](<https://en.wikipedia.org/wiki/Snake_(video_game_genre)>) |
| 8 | +jejíž princip je tu s námi od roku 1976. Největší popularity se Had dočkal |
| 9 | +díky mobilním telefonům Nokia, kde je jako základní hra dostupný od roku 1998 |
| 10 | +až dodnes. |
| 11 | + |
| 12 | +Projekt není zase tak složitý, protože jeho základní principy už dobře znáš |
| 13 | +z domácích projektů a lekcí kurzu. Následující text je tedy spíše |
| 14 | +zadání než výukový materiál a v projektu jistě narazíš na něco, co jsme |
| 15 | +společně neprobírali. V takovém případě se neboj zeptat nebo si informace |
| 16 | +dohledat! |
| 17 | + |
| 18 | +A ještě jedna věc: protože začátečnický kurz končí, |
| 19 | +začneme kód psát v angličtině, aby se pak dal sdílet s celým světem. |
| 20 | + |
| 21 | +> [note] |
| 22 | +> Procházíš-li si projekt doma, je možné, že narazíš na |
| 23 | +> něco s čím si nebudeš vědět rady. |
| 24 | +> Kdyby se to stalo, prosím, ozvi se nám! |
| 25 | +> Rádi ti s projektem pomůžeme. |
| 26 | +
|
| 27 | +## Logika hry a fáze projektu |
| 28 | + |
| 29 | +Základní princip hry máš v malíčku, pokud jsi dokončil{{a}} domácí projekt |
| 30 | +po [lekci o seznamech](../../beginners/list/). Pokud jej nemáš, doporučuji |
| 31 | +se k němu vrátit. |
| 32 | + |
| 33 | +Práci s pygletem jsme dělali v [lekci o grafice](../../intro/pyglet/). |
| 34 | + |
| 35 | +Teď nám nezbývá než princip tolik populární hry a znalosti z kurzu spojit |
| 36 | +dohromady. Doporučuji začít s čistým souborem v prázdné složce a do hotových |
| 37 | +programů se koukat jen v případě potřeby. |
| 38 | + |
| 39 | +Jak postupovat, aby se projekt nezdál nedosažitelný už na začátku? Třeba takto: |
| 40 | + |
| 41 | +0. Promysli si, jak bude hra fungovat a jak přeneseme mřížku s hadem |
| 42 | +z příkazové řádky do grafického okna. |
| 43 | +1. Vykresli hada do grafického okna (ve formě barevných čtverců) |
| 44 | +2. Přidej funkci, která bude hadem hýbat. |
| 45 | +3. Umožni změnit směr hada pomocí klávesnice. |
| 46 | +4. Nenech hada utéct z herní plochy a nabourat do sebe sama. |
| 47 | +5. Přidej hadovi jídlo a zajisti, aby po jídle rostl. |
| 48 | +6. Vyměň barevné čtverečky za opravdovou grafiku. |
| 49 | + |
| 50 | +Po těchto krocích budeš mít základní hru, ale tou to nekončí, právě naopak! |
| 51 | +Budeš mít vlastní hru, jejímuž fungování rozumíš jako nikdo jiný, a to je to pravé pro |
| 52 | +přidávání dalších možností. Fantazii se meze nekladou. Například: |
| 53 | + |
| 54 | +1. Ve hře mohou být dva nebo třeba tři hadi najednou – každý ovládaný |
| 55 | +jinými klávesami — navzájem soupeřící o jídlo. |
| 56 | +2. Kromě jídla se mohou na ploše objevovat i jiné objekty – překážky, |
| 57 | +do kterých nesmí had narazit, otrávené jídlo, které hada zkrátí atp. |
| 58 | +3. Hrací plocha může být nekonečná a když z ní had vyleze, objeví se |
| 59 | +na druhé straně. |
| 60 | + |
| 61 | +## Z příkazové řádky do grafické aplikace |
| 62 | + |
| 63 | +V příkazové řádce měl had souřadnice označující řádek a sloupec. V grafické |
| 64 | +aplikaci to bude podobné, ale protože pixelů na obrazovce je mnohem více, budeme |
| 65 | +si muset vytvořit pomyslnou síť stejně velkých čtverců, které nám nahradí |
| 66 | +řádky a sloupce. Velikost takového čtverce bude konstanta, kterou se vyplatí |
| 67 | +mít po celou dobu hry k dispozici, aby se podle ní daly vypočítat |
| 68 | +souřadnice k vykreslení obrázků. Pro začátek řekněme, že ideální velikost |
| 69 | +takového čtverce bude 64 × 64 pixelů. |
| 70 | + |
| 71 | +Z velikosti čtverce, kterou si můžeme v budoucnu libovolně změnit, |
| 72 | +a velikosti okna aplikace můžeme vypočítat, kolik se nám do okna takových |
| 73 | +čtverců vejde na šířku a na výšku a tím i zjistit, kolik pomyslných |
| 74 | +sloupců a řádků bude naše hrací plocha mít. |
| 75 | + |
| 76 | +## Vykreslení hada |
| 77 | + |
| 78 | +Abychom mohli hada vykreslit, potřebujeme si pro začátek uložit jeho souřadnice. |
| 79 | +K tomu můžeš použít seznam dvojic – stejně jako v domácím projektu. Podobných |
| 80 | +informací, které se budou v průběhu hry dynamicky měnit, budeme mít už |
| 81 | +za malou chvíli více. Proto dává smysl si pro stav hry vytvořit třídu, která |
| 82 | +bude tyto informace obsahovat jako atributy a bude s nimi umět pracovat. |
| 83 | + |
| 84 | +> [note] |
| 85 | +> I když se může na začátku zdát vlastní třída jako zbytečná |
| 86 | +> komplikace, později zjistíš, že ne všechno by se dalo snadno udržovat v globálních |
| 87 | +> proměnných. |
| 88 | +
|
| 89 | +{{ figure( |
| 90 | + img=static('coords.svg'), |
| 91 | + alt="Had na „šachovnici“ se souřadnicemi", |
| 92 | +) }} |
| 93 | + |
| 94 | +Když už je had definován, budeme potřebovat jednoduchou funkci, která |
| 95 | +na ta správná místa umístí obrázky. Pro začátek si vystačíme se zeleným |
| 96 | +čtvercem. Obrázek si [stáhni zde]({{ static('green.png')}}) a ulož |
| 97 | +do složky k programu. |
| 98 | + |
| 99 | +Stejně jako na lekci i zde použijeme pro vykreslení `Sprite`, kterému už při |
| 100 | +vytvoření můžeme zadat obrázek pro vykreslení a vypočtené souřadnice. |
| 101 | +Pro jednoduchost stačí `Sprite` vytvořit, vykreslit a „zapomenout“. Není to ale |
| 102 | +optimální přístup a tak tohle může být jedním z adeptů pro pozdější vylepšení. |
| 103 | + |
| 104 | +## Rozpohybování hada |
| 105 | + |
| 106 | +Aby se mohl had hýbat, potřebuje znát směr pohybu. V příkazové řádce jsme |
| 107 | +vždy počkali, až nám směr zadá uživatel, ale v opravdové hře se bude had |
| 108 | +pohybovat sám. Bude tedy potřeba nějaký atribut v naší třídě, kde bude směr |
| 109 | +neustále uložen a měnit se bude podle stisknutých kláves v dalším kroku. |
| 110 | + |
| 111 | +Směr pohybu může být uložen v libovolné podobě – světové strany, slovní |
| 112 | +označení strany, nebo třeba dvojice s číselným označením pohybu |
| 113 | +(`(0, 1)` pro pohyb nahoru, `(-1, 0)` pro pohyb doleva atp.). Podle vybraného |
| 114 | +formátu pak bude třeba směr zpracovat. |
| 115 | + |
| 116 | +Pro tuhle chvíli mu tedy bude stačit nastavit směr napevno a napsat funkci, |
| 117 | +nebo metodu, která hadem pohne. Pohyb bude probíhat |
| 118 | +naprosto stejně jako v příkazové řádce – přidáme do seznamu souřadnice, |
| 119 | +kde by měla být „nová hlava“ a umažeme poslední kousek hada. |
| 120 | + |
| 121 | +Protože se pohyb má provádět pravidelně, bude potřeba tuto operaci provádět |
| 122 | +automaticky v pravidelných intervalech. `pyglet.clock.schedule_interval` je |
| 123 | +zde jasná volba. |
| 124 | + |
| 125 | +## Ovládání pomocí klávesnice |
| 126 | + |
| 127 | +Reagovat na stisknuté klávesy jsme se už taky učili. Teď to tedy využijeme, |
| 128 | +abychom dokázali změnit nastavený směr pohybu z předchozího bodu. Bude pro to |
| 129 | +samozřejmě potřeba funkce, kterou v pygletu zaregistrujeme pro spuštění po |
| 130 | +stisku klávesy. |
| 131 | + |
| 132 | +Protože had už se nám v závislosti na směru pohybuje, měl by začít reagovat |
| 133 | +na jeho změnu. |
| 134 | + |
| 135 | +V tuto chvíli: |
| 136 | + |
| 137 | +* Směr se mění podle stisknuté klávesy. |
| 138 | +* Had se sám pohybuje podle zadaného směru. |
| 139 | +* Nová pozice hada se automaticky vykresluje jako zelené čtverečky. |
| 140 | + |
| 141 | +Vida, máme hotový základ! |
| 142 | + |
| 143 | +## Nenechme ho utéct |
| 144 | + |
| 145 | +Had už se nám hýbe podle našich představ, ale stačí ho nechat chvíli bez dozoru |
| 146 | +a uteče nám z hrací plochy. Tomu není těžké zabránit, když víme, že žádná |
| 147 | +souřadnice hada nesmí být menší než nula a větší než je velikost hrací plochy. |
| 148 | +Kontrolovat je potřeba souřadnice jeho hlavy, která bude vždy všude jako první. |
| 149 | + |
| 150 | +Reagovat na náraz do zdi se dá mnoha způsoby. Nejjednodušší by asi bylo |
| 151 | +ukončit hru, ale to by se pak hráč nemohl podívat na tu šlamastiku, do které |
| 152 | +se dostal. Proto bude lepší místo toho pouze zastavit časovač, který se stará |
| 153 | +o pohyb hada. |
| 154 | + |
| 155 | +Stejným způsobem a na stejném místě v programu bude třeba vyřešit i situaci, |
| 156 | +kdy had narazí sám do sebe. |
| 157 | + |
| 158 | +## Jen ať jí, hlavně že mu chutná |
| 159 | + |
| 160 | +Jezdit s hadem po hrací ploše může být chvíli zábava, ale protože had neroste, |
| 161 | +není to žádná výzva. A aby mohl růst, potřebuje jíst. |
| 162 | + |
| 163 | +K tomu budeš potřebovat další globálně dostupný seznam (nejlépe atribut |
| 164 | +existující třídy), který bude obsahovat informace (souřadnice) o existujícím |
| 165 | +jídle na hrací ploše. Navíc bude potřeba mít k dispozici metodu, která bude |
| 166 | +umět jídlo na hrací plochu přidat. |
| 167 | + |
| 168 | +Záleží jen na tobě, zda se bude nové jídlo objevovat, když had jedno |
| 169 | +z existujících sní, nebo automaticky v pravidelných intervalech. |
| 170 | + |
| 171 | +Jídlo vykreslíme stejným způsobem jako hada (ve stejné funkci/metodě) a jako |
| 172 | +obrázek použijeme třeba [jablko]({{ static('apple.png')}}). |
| 173 | + |
| 174 | +První závan grafiky :-) |
| 175 | + |
| 176 | +## Čtverečky ven, grafiku sem |
| 177 | + |
| 178 | +Čtverečky jsou fajn, ale hra by měla lahodit oku a had by měl vypadat jako had. |
| 179 | +K tomu máme připravenou sadu obrázků - [ke stažení zde]({{ static('snake-tiles.zip') }}). |
| 180 | +Archiv si rozbal do adresáře s hrou tak, aby adresář `snake-tiles` byl na stejné |
| 181 | +úrovni jako soubor s programem. |
| 182 | + |
| 183 | +{{ figure( |
| 184 | + img=static('snake-tiles.png'), |
| 185 | + alt="Kousky hada", |
| 186 | +) }} |
| 187 | + |
| 188 | +### Načtení všech obrázků ze složky |
| 189 | + |
| 190 | +Nejdříve si načteme všechny obrázky do hry, abychom je pak mohli bez potíží |
| 191 | +použít. Protože se nechceme opakovat (DRY), bude potřeba to udělat nějak |
| 192 | +poloautomaticky. Python obsahuje knihovnu [`pathlib`](https://docs.python.org/3/library/pathlib.html), |
| 193 | +která umí velmi přehledně pracovat s cestami k souborům a třeba nám dát |
| 194 | +i seznam všech souborů ve složce. |
| 195 | + |
| 196 | +Nejdříve si z této knihovny naimportujeme třídu `Path`, která reprezentuje |
| 197 | +soubor či složku na disku a vytvoříme z ní instanci, která bude |
| 198 | +ukazovat do naší složky s obrázky. |
| 199 | + |
| 200 | +```python |
| 201 | +from pathlib import Path |
| 202 | + |
| 203 | +TILES_DIRECTORY = Path('snake-tiles') |
| 204 | +``` |
| 205 | + |
| 206 | +Třída `Path` má metodu `glob()`, která nám ze zadané cesty umí vrátit sekvenci |
| 207 | +s názvy souborů dle argumentem zadaných kritérií. My potřebujeme všechny soubory |
| 208 | +s příponou `.png` bez ohledu na jméno. Jakýkoli řetězec je v regulárních |
| 209 | +výrazech označen hvězdičkou (`*`), takže argument pro metodu `glob()` bude |
| 210 | +`*.png`, což označuje jakýkoli soubor s příponou `.png`. Jako výsledek |
| 211 | +dostaneme sekvenci cest k souborům s obrázky, kterou můžeme projít pomocí cyklu |
| 212 | +`for`, a každý obrázek si můžeme načíst do slovníku, kde hodnotou bude samotný obrázek |
| 213 | +`pyglet.image` a klíčem jeho název. Z názvu však potřebujeme jen samotný název souboru |
| 214 | +bez přípony a názvu složky – ten je uložen v atributu `stem`. |
| 215 | + |
| 216 | +Výsledný slovník by měl vypadat takto: |
| 217 | + |
| 218 | +``` |
| 219 | +{'right-tongue': <ImageData 64x64>, 'top-tongue': <ImageData 64x64>, |
| 220 | + 'right-top': <ImageData 64x64>, 'left-bottom': <ImageData 64x64>, |
| 221 | + 'tail-left': <ImageData 64x64>, 'bottom-tongue': <ImageData 64x64>, |
| 222 | + 'left-top': <ImageData 64x64>, 'bottom-bottom': <ImageData 64x64>, |
| 223 | + ... |
| 224 | +``` |
| 225 | + |
| 226 | +Pokud je tohle pro tebe příliš mnoho nových věcí najednou a nedaří se ti to |
| 227 | +vyřešit, zkus to ještě jednou a pak se můžeš podívat na řešení. |
| 228 | + |
| 229 | +{% filter solution %} |
| 230 | +```python |
| 231 | +from pathlib import Path |
| 232 | + |
| 233 | +import pyglet |
| 234 | + |
| 235 | +TILES_DIRECTORY = Path('snake-tiles') |
| 236 | + |
| 237 | +snake_tiles = {} |
| 238 | +for path in TILES_DIRECTORY.glob('*.png'): |
| 239 | + snake_tiles[path.stem] = pyglet.image.load(path) |
| 240 | + |
| 241 | +print(snake_tiles) |
| 242 | +``` |
| 243 | +{% endfilter %} |
| 244 | + |
| 245 | +### Housenka |
| 246 | + |
| 247 | +Než se začneme zabývat různými obrázky, uděláme pokus k ověření, že nám vše stále funguje. |
| 248 | +Jako mezistupeň od hranatého hada k jeho věrné grafické podobě vytvoř housenku. |
| 249 | +Uděláš to jednoduše tak, že místo zeleného čtverce použiješ k vykreslení hada |
| 250 | +obrázek `tail-head.png`, který máš ve slovníku načten jako pod klíčem `tail-head`. |
| 251 | + |
| 252 | +Funguje? No výborně! Před pokračováním si u jeho hraní na chvíli odpočiň. |
| 253 | +Začne to být náročnější. |
| 254 | + |
| 255 | +{{ figure( |
| 256 | + img=static('screenshot-cat.png'), |
| 257 | + alt="Housenka", |
| 258 | +) }} |
| 259 | + |
| 260 | +### Výběr správných obrázků |
| 261 | + |
| 262 | +Jistě sis všiml{{a}}, že některé obrázky v naší sadě jsou téměř identické a liší |
| 263 | +se jen v otočení. V tuhle chvíli máme totiž dvě možnosti, jak vykreslit |
| 264 | +celého hada pomocí správných obrázků na správných pozicích: |
| 265 | + |
| 266 | +1. Můžeme vzít jeden obrázek pro tělo, jeden pro ohyb a po jednom pro |
| 267 | +hlavu a ocas a ty otáčet tak, jak to bude pro konkrétní kousek hada potřeba. |
| 268 | +2. Můžeme využít všech dostupných (různé otočených) obrázků a použít ten |
| 269 | +správný obrázek na tom správném místě. |
| 270 | + |
| 271 | +Bod č. 2 je v tuto chvíli snazší a tak budeme pokračovat tímto způsobem. |
| 272 | + |
| 273 | +Jak vybrat správné obrázky na ta správná místa? Jména obrázků (klíče ve slovníku) |
| 274 | +obsahují informaci, odkud kam daný obrázek vede. Stačí se tedy při |
| 275 | +vykreslování každého kousku hada podívat na umístění jednoho před ním |
| 276 | +a jednoho za ním a podle toho vybrat ze slovníku ten správný obrázek. |
| 277 | +U každého kousku hada a kousku před i za ním tě budou zajímat jejich |
| 278 | +souřadnice, protože podle nich lze velmi snadno poznat, zda je zkoumaný kousek |
| 279 | +nalevo, napravo, nahoře, nebo dole. |
| 280 | + |
| 281 | +Způsobů, jak toho docílit, je celá řada a i když se to může zdát jako složitější |
| 282 | +úkol, vše potřebné k jeho vyřešení znáš. |
| 283 | + |
| 284 | +{{ figure( |
| 285 | + img=static('screenshot-final.png'), |
| 286 | + alt="Finální had", |
| 287 | +) }} |
| 288 | + |
| 289 | +Odměnou za vyřešení ti bude kompletní grafická hra Had. Gratuluji! |
| 290 | + |
| 291 | +## Optimalizace, úklid |
| 292 | + |
| 293 | +Než se po dokončení základní hry vrhneš na její rozšiřování, měl by se celý kód |
| 294 | +uklidit a zpřehlednit, aby se v něm další úpravy dělaly snáze a s menším |
| 295 | +rizikem, že se něco pokazí. |
| 296 | + |
| 297 | +Body k zamyšlení: |
| 298 | + |
| 299 | +* Pokud se ti tam opakuje nějaký kousek kódu vícekrát, možná by se dal |
| 300 | +vložit do funkce nebo cyklu. |
| 301 | +* Mají všechny proměnné smysluplná jména? |
| 302 | +* Při vykreslování možná tvoříš pro každý kousek hada nový `Sprite` a ten |
| 303 | +je po vykreslení zapomenut. Optimálnější by možná bylo použít seznam |
| 304 | +a v něm všechny instance třídy `Sprite` uchovávat a používat znovu a znovu. |
| 305 | +`Sprite` přeci můžeme posunout na libovolné místo i změnit obrázek, který |
| 306 | +obsahuje. |
| 307 | +* Používáš globální proměnné? Nebylo by lepší mít jednu třídu pro stav hry |
| 308 | +a v ní všechny podstatné informace a metody? |
| 309 | +* Funguje ovládání dle tvých představ nebo by šlo nějak zlepšit? |
0 commit comments