Princip hry
Tato hra je velmi známá, někdy se jí říká housenka. Hlavním aktérem naší verze bude Hungry Snake - hladový had. Protože ještě nic nejedl, je strašně malinký - skládá se ze tří článků. Had umí lézt a měnit směr pohybu. Cílem hry je nakrmit hada, to znamená nasměrovat ho na nejbližší pamlsek. Důležité je, aby had nenarazil do stěny ani do sebe. Vždy, když sežere nějaký pamlsek, na konci mu přibude nový článek. Tak se pokračuje, doku had nenarazí, nebo dokud není tak dlouhý, že se již do místnosti nevejde.
Pro zjednodušení předpokládejme, že místnost má velikost 10x10 políček, každé políčko je čtvercové a jeden článek hada se vejde právě na jedno políčko. V každém kroku hry se hlava hada může posunout do tří směrů vzhledem ke směru pohybu - rovně, doprava a doleva. Každý článek hada se pak přesune na pozici článku předchozího, to znamená, že články hada jedou stejnou cestou, jako hlava.
Příprava formuláře
Protože hra neobsahuje moc animací a pohybu, stačí nám vykreslování grafiky přes rozhraní GDI+, které je součástí .NET frameworku, se kterým pracujeme.
Nejdříve si ale upravíme formulář. Jeho vlastnost Size (velikost formuláře) nastavte na hodnotu 450;480 a jeho vlastnost FormBorderStyle (typ okna) nastavte na FixedSingle (uživatel nebude moci měnit velikost okna). Nakonec nastavte jeho vlastnost MaximizeBox na False, abyste zakázali tlačítko Maximalizovat.
Na formulář umístěte komponentu PictureBox , na kterou budeme vykreslovat herní scénu. Tato komponenta je přímo určena pro zobrazování grafiky a můžeme do ní jednoduše kreslit.
Důležité je uvědomit si, že vykreslování je časově poměrně náročná operace a rozhodně se musíme snažit vykreslovat co nejméně. Tím spíš v rozhraní GDI+, které je sice určeno pro grafiku, ale přece jen nedosahuje zdaleka tak dobrých výsledků, jako třeba DirectX.
Velikost PictureBoxu nastavte na 420;420. Bylo by dobré, aby se PictureBox zarovnal na střed formuláře. To provedeme jednoduše - v menu Format vyberte možnost Center In Form a vyberte nejdříve Horizontally a pak znovu i Vertically. Tím se nám komponenta zarovná na střed formuláře.
Použití obrázků
Abychom mohli začít, musíme si sehnat obrázky. Já jsem pro Vás nějaké připravil (nejsou sice nic moc, ale snad to bude stačit).
Na formulář si nyní vytvoříme komponentu ImageList, kterou najdeme v soupravě nástrojů v kategorii Components. Tato komponenta umožňuje načíst obrázky do paměti a ostatní objekty s nimi pak mohou pracovat. ImageList je komponenta, která se nijak nezobrazuje (nemá vzhled) a umístí se na spodní lištu neviditelných objektů.
Objektu ImageList nastavíme vlastnost ImageSize na 35;35 (velikost našich obrátků), dále vlastnost ColorDepth na Depth24Bit (vysoká kvalita obrázků) a vlastnost TransparentColor (průhledná barva) na White (nejjednodušší je hodnotu vypsat ručně nebo vybrat na záložce Palette bílou barvu úplně vlevo v rohu).
Pak v okně vlastností vyberte vlastnost Images a klikněte na tlačítko se třemi tečkami. Objeví se okno, ve kterém nahrajeme do komponenty obrázky. Nahrajte obrázky, které jste si stáhli, tlačítkem Add v abecedním pořadí (bonus1, bonus2, bonus3, bonus4, snake_b, snake_h, wall). Nakonec potvrďte tlačítkem OK.
A jak to udělat, aby bylo aspoň něco vidět?
Nyní dvakrát klikněte na formulář (ale ne tam, kde je PictureBox), aby se zobrazila procedura události Form_Load. Pro kreslení budeme používat jmenný prostor System.Drawing. Jmenné prostory slouží mimo jiné k tomu, aby se všechny třídy daly snadno rozdělit podle toho, co dělají. Jmenný prostor System.Drawing tedy obsahuje vše, co se týká grafiky. Nejdůležitější třídy jsou Image, Bitmap a Graphics. Pomocí těchto tříd zajistíme veškeré grafické funkce pro naši hru.
Image a Bitmap jsou třídy, které reprezentují obrázek. Obsahují funkce pro jeho vytvoření, načítání a ukládání. Třída Graphics je jakési kreslící plátno, které nám umožňuje vytvářet kresby pomocí čar, elips, oblouků, obdélníků a mnohoúhelníků, různých barevných přechodů a efektů atd.
Poznámka: Třídu si můžeme představit jako jakousi šablonu pro objekt. O objektu víme, že je to sada nějakých vlastností, funkcí a procedur. Takový objekt je například komponenta na formuláři, nebo třeba i obrázek, který jsme nahráli ze souboru. Abychom vytvořili objekt, musíme říci, podle jaké třídy se vytvoří. A k tomu potřebujeme konstruktor New. Takže pokud napíšu Dim a As New Bitmap(420,420), vytvoří se nový objekt podle třídy Bitmap a zavolá se jeho funkce New s parametry 420,420. Objektů můžeme vytvořit libovolné množství. Například i Form1 je třída a můžeme z ní vytvořit tolik objektů, kolik budeme potřebovat. Kolik objektů vytvoříme, tolik formulářů budeme mít. Většinou nám ale stačí jeden.
Některé třídy mají i tzv. statické funkce, které můžeme použít, aniž bychom museli vytvářet objekt.
Procedura události OnPaint
Některé komponenty mají událost OnPaint, která se spouští, když je potřeba obsah této komponenty vykreslit na obrazovku. Typicky k tomu dochází při obnovení minimalizované aplikace, nebo při zavření nebo schování jiného okna, které to naše překrývalo. Pokud tedy do události OnPaint zapíšeme kód pro překreslení celé scény, máme zaručeno, že scéna bude na formuláři vidět pořád. Tuto událost můžeme uměle vyvolat také zavoláním metody Invalidate, které překreslí celou komponentu.
Drobnější změny je ale zbytečné překreslovat touto událostí a někdy to vyvolává i nepříjemný efekt problikávání.
Kreslící plátno Graphics je předáváno jako argument v argumentu procedury e. K tomuto plátnu tedy přistupujeme přes e.Graphics.
Vykreslování přímo
Kreslící plátno komponenty můžeme také získat voláním metody CreateGraphics. Změny na komponentě se projeví okamžitě. Jakmile ale okno schováme a znovu skryjeme, spustí se zase událost OnPaint, která musí vykreslit celou scénu.
Vykreslení zdí místnosti
Velikost PictureBoxu jsme nastavili na 420 x 420 pixelů. Velikost obrázků grafiky je 35 x 35 pixelů, to znamená, že se do PictureBoxu vejde 12 x 12 políček. Kolem místnosti uděláme zdi a mám hrací plochu 10 x 10 políček, což jsme na začátku chtěli.
Dvakrát klikněte na komponentu PictureBox na formuláři a v rozbalovacích seznamech nahoře v okně kódu najděte událost Paint. Do ní zapište tento kód:
With e.Graphics
.Clear(Color.White)
For i As Integer = 0 To 10
.DrawImage(ImageList1.Images(6), i * 35, 0)
.DrawImage(ImageList1.Images(6), (i + 1) * 35, 11 * 35)
.DrawImage(ImageList1.Images(6), 0, (i + 1) * 35)
.DrawImage(ImageList1.Images(6), 11 * 35, i * 35)
Next
End With
První a poslední řádek je With blok, který pracuje s objektem e.Graphics a místo psaní e.Graphics.něco stačí uvnitř bloku napsat .něco. Metoda Clear maže celé kreslící plátno a nastaví mu zvolenou barvu (v našem případě bílá).
Dále máme cyklus, který kreslí všechny 4 zdi arény. Velikost arény je 12 x 12, a strany jsou 4 a rohy také, takže jedna strana bude mít 11 políček. To dvanácté (druhý roh) bude již patřit další straně. Metoda DrawImage potřebuje 3 argumenty - obrázek (objekt typu Image, v našem případě je to sedmý obrázek z komponenty ImageList), který se má vykreslit a souřadnice X a souřadnici Y na plátně, kam má přijít horní levý roh obrázku.
První řádek v cyklu tedy vykreslí horní stranu. Proměnná i nabývá hodnot od 0 do 10, souřadnice Y je pořád nula (horní roh obrázku bude na plátně úplně nahoře) a souřadnice X nabývá hodnot násobků 35 (protože jeden obrázek má 35x35 pixelů), tím pádem se nám obrázek vykreslí 11x vedle sebe. Druhý řádek je spodní strana. Souřadnice Y je pořád 11 x 35, protože nad těmito obrázky musí být prostor pro 11 políček o velikosti 35 pixelů. Souřadnice X jsou také v násobcích 35, ale první z nich je až 35, protože spodní levý roh patří levé stěně. Další dva řádky fungují obdobně, názorně je to vidět na obrázku. Ještě zbývá doplnit, že souřadnice [0,0] je v horním levém rohu kreslícího plátna. Souřadnice na obrázku nejsou v pixelech, jednotka je velikost 1 obrázku, tedy 35 pixelů. My ale pracujeme v pixelech, je tedy nutné vše vynásobit velikostí obrázku, tedy 35 pixely.
Jak udělat hada?
Zdi arény máme vykreslené, teď nám zbývá udělat hada. Musíme mít proměnnou pro směr pohybu hlavy, pole pozic článků hada a počet článků hada. O každém políčku hrací plochy musíme vědět, jestli je volné, jestli na něm je nějaký článek hada, nebo jestli na něm je bonus.
Do globálních deklarací formuláře (pod úplně první řádek v okně kódu, mimo procedury) přidejte tento kód, který nám globálně (pro celý formulář) nadeklaruje tyto proměnné, datové struktury a pole konstant.
Enum POLE
Volno = 0
Had = 1
Bonus = 2
End Enum
Enum SMERY
Nahoru = 1
Dolu = 2
Doleva = 3
Doprava = 4
End Enum
Structure XY
Dim X As Integer
Dim y As Integer
End Structure
Dim Plocha(10, 10) As POLE
Dim Had(100) As XY
Dim Delka As Integer
Dim Smer as SMERY
Enumeration - pole konstant
Pokud máme pro jednu proměnnou více různých hodnot, kde každá znamená něco úplně jiného (co se dá třeba popsat slovem), pro větší přehlednost můžeme použít Enum - seznam hodnot. Proměnnou pak nadeklarujeme ne jako Integer, ale jako název tohoto seznamu hodnot. Jak se enumy deklarují, je vidět z ukázky. Enumy ale nemusí být pouze pro Integer, ale třeba pro String.
Structure - datová struktura
Pokud si do jedné proměnné potřebujeme uložit více hodnot, máme možnost použít Structure. Do ní napíšeme deklarace dílčích hodnot, ze kterých se požadovaná informace skládá. Proměnnou pak nadeklarujeme jako název této struktury. V našem příkladu jsme si vytvořili pole datového typu XY. A struktura XY obsahuje dvě hodnoty typu Integer. Takže pokud mám třeba šestý článek hada, který je na pozici [6,4], uložím jej takto:
Had(5).X = 6
Had(5).Y = 4
Připomínám, že pole se ve Visual Basicu .NET indexují od nuly, takže přestože měním 6. článek hada, manipuluji s prvkem číslo 5, první je totiž prvek číslo 0.
Nastavení počáteční pozice hada
Dvakrát klikněte na formulář a do procedury Form_Load zapište tento kód:
Had(0).X = 5 : Had(0).y = 5
Had(1).X = 5 : Had(1).y = 6
Had(2).X = 5 : Had(2).y = 7
Plocha(5, 5) = POLE.Had
Plocha(5, 6) = POLE.Had
Plocha(5, 7) = POLE.Had
Delka = 3
Smer = SMERY.Nahoru
První tři řádky nastaví pozice prvních tří článků hada. Všimněte si, že když chceme zapsat na řádek více příkazů, oddělíme je dvojtečkou.
Další tři řádky nastaví informace políček, na kterých jsou články hada, na hodnotu POLE.Had, což znamená, že toto políčko je obsazeno hadem. To se nám bude hodit, až budeme řešit narážení hada do sebe samotného.
Předposlední řádek nastaví počet článků hada a poslední řádek nastaví směr pohybu hlavy.
Nyní musíme ještě do procedury Paint přidat vykreslení celého hada.
Přidejte za vykreslení zdí, ale pořád ještě do With bloku, tento kód:
.DrawImage(ImageList1.Images(4), Had(0).X * 35, Had(0).y * 35)
For i As Integer = 1 To Delka - 1
.DrawImage(ImageList1.Images(5), Had(i).X * 35, Had(i).y * 35)
Next
Když nyní hru spustíme, uvidíme hada v počáteční pozici a místnost obklopenou zdmi.
Pohyb hada
Nahoře si najděte záložku Form1.vb [Design] a kliknutím na ni přejdete do režimu návrhu formuláře.
Na formulář přidejte komponentu Timer. Její vlastnost Interval nastavte na 300, to znamená, že se bude had pohybovat o jedno políčko za 300 milisekund, tj. za 0,3 sekundy.
Nyní na objekt Timer1 dvakrát klikněte a do procedury události Timer1_Tick, která se spustí každých 300 milisekund, pokud bude časovač zapnutý, napište tento kód:
Dim g As Graphics = PictureBox1.CreateGraphics()
g.FillRectangle(Brushes.White, Had(Delka - 1).X * 35, Had(Delka - 1).y * 35, 35, 35)
Plocha(Had(Delka - 1).X, Had(Delka - 1).Y) = PLOCHA.Volno
For i As Integer = Delka To 1 Step -1
Had(i).X = Had(i - 1).X
Had(i).y = Had(i - 1).y
Next
g.DrawImage(ImageList1.Images(5), Had(0).X * 35, Had(0).y * 35)
Select Case Smer
Case SMERY.Nahoru
Had(0).y = Had(0).y - 1
Case SMERY.Dolu
Had(0).y = Had(0).y + 1
Case SMERY.Doleva
Had(0).X = Had(0).X - 1
Case SMERY.Doprava
Had(0).X = Had(0).X + 1
End Select
g.DrawImage(ImageList1.Images(4), Had(0).X * 35, Had(0).y * 35)
Musíme si uvědomit, jak se vůbec had pohybuje. Nejprve se hlava posune směrem, kterým se posunovat má. Druhý článek se dostane na její pozici. Třetí článek jde na pozici druhého, čtvrtý na pozici třetího, a tak to jde dále, až se poslední článek dostane na pozici, kde byl předposlední.
My si ale musíme navíc celý postup obrátit, protože pokud posuneme hlavu jinam - změníme hodnoty v proměnné Had(0), už nevíme, kde byla před tím. Jedině že bychom si to uložili do nějaké pomocné proměnné. To je ale trochu neobratné. Pokud nejdřív přiřadíme poslednímu článku pozici článku předchozího, předposlednímu opět pozici článku předchozího, až druhému článku přiřadíme pozici hlavy, můžeme bez obav posunout hlavu a proces je hotov. Nepotřebujeme k tomu žádné další proměnné. A to, že dva články v poli mají v jednu chvíli stejnou pozici, vůbec nevadí. Ještě pro doplnění - pokud mám i-tý článek, tak předchozí bude mít index i - 1. Proto můžu použít cyklus. Ještě jste si mohli všimnout, že definice cyklu obsahuje na konci Step -1. To použijeme v případě, že potřebujeme procházet opačným směrem. Normálně se přičítá k i jednička, ale pokud nastavíme Step -1, bude se přičítat -1, takže i bude postupně klesat od Delka až do 1, kdy na pozici Had(1) přiřadí Had(i-1), čili Had(0), čili pozici hlavy hada.
Pak přes rozhodovací strukturu Select Case rozhodneme, kdy posunout hlavu nahoru a kdy dolů, kdy doprava a kdy doleva. Ještě zbývá vysvětlit, jak funguje posun hlavy daným směrem. Pokud se hlava pohybuje nahoru, klesá její souřadnice Y. Protože se nepohybuje do boku, není třeba vůbec měnit souřadnici X. Pokud leze doprava nebo doleva, mění se zase jenom souřadnice X a souřadnice Y zůstává stejná jako v předchozím kroku.
A jak tuto situaci vykreslovat? Nejprve si zařídíme přímý přístup na plátno vytvořením proměnné g typu Graphics a zavoláním metody CreateGraphics. Nemusíme totiž překreslovat celého hada, protože prostřední články se vůbec nemění. Stačí tedy na obrázku smazat poslední článek (g.FillRectangle(Brushes.White, x1, y1, w, h) vyplní obdélníkovou oblast zadanou bodem [x1,y1], což je horní levý roh oblasti, její výškou w a šířkou h, a to celé bílou barvou - Brushes.White). V poli Plocha musíme tomuto políčku nastavit hodnotu Volno, protože na něm již had není.
Dále je třeba překreslit původní hlavu, na tomto místě nyní bude normální článek. Článek má totiž jiný obrázek než hlava hada, aby bylo poznat, kde hlavu had má. Jakmile hlavu posuneme, na novou pozici, musíme hlavu vykreslit a navíc do pole Plocha musíme na novou pozici nastavit hodnotu Had. Ještě zbývá dodat, že počet článků hada je Delka a pole Had se indexuje od nuly. To znamená, že poslední článek je uložen v buňce Had(Delka - 1). Možná jste si také všimli, že cyklus běží od Delka do 1, i když by stačilo, aby běžel od Delka - 1. To však má svůj důvod. Protože na políčku Had(Delka) budeme mít uložené políčko, které had právě opustil. Pokud tedy sežere bonus a budeme jej chtít prodloužit o jeden článek, budeme vědět, kam se má nový článek přidat (resp. stačí pouze zvětši proměnnou Delka o 1, článek vykreslit a nastavit pole Plocha, v poli Had už pozici článku máme nastavenou).
Ovládání hada
Toto je poslední, co v tomto dílu uděláme. Zbytek bude v dílu dalším. Abychom hada mohli ovládat, potřebujeme pracovat s klávesami. Pokud uživatel stiskne klávesu a na formuláři není nějaký objekt, který by mohl klávesy filtrovat (třeba TextBox nebo tlačítko), na formuláři se spouští události Form_KeyDown, Form_KeyUp a Form_KeyPress. Událost KeyDown se spustí, když klávesu stisknete, KeyUp se spustí, když klávesu pustíte. KeyPress se opakovaně spouští, dokud klávesu držíte. Zkuste si v textovém poli podržet třeba klávesu s písmenem a. Každé spuštění události KeyPress odpovídá jednomu napsanému písmenku.
My budeme potřebovat pouze událost KeyDown, takže dvakrát klikněte na formulář a v rozbalovacím seznamu nahoře vyberte KeyDown. V procedurách zpracovávajících klávesy můžeme zjistit, která klávesa událost vyvolala, z proměnné KeyCode, která je uložená v argumentu e. Do procedury KeyDown tedy zapište tento kód:
Select Case e.KeyCode
Case Keys.Up
Smer = SMERY.Nahoru
Case Keys.Down
Smer = SMERY.Dolu
Case Keys.Left
Smer = SMERY.Doleva
Case Keys.Right
Smer = SMERY.Doprava
End Select
A to je celé. Podle toho, co je v e.KeyCode, se nám nastaví směr pohybu hlavy. A máme skoro hotovo. V příštím dílu našeho hada dokončíme - přidáme funkci kontroly nárazů (pokud had narazí do zdi nebo do sebe, hra skončí) a také bonusy, které bude moci had sníst.
Pro tento díl je to vše.