Bubble Breaker

3. díl - Bubble Breaker

Tomáš Herceg       01.06.2007       VB.NET, WinForms, Grafika       16535 zobrazení

V tomto díle si ukážeme, jak se dělají barevné přechody s GDI+, vysvětlíme si, jak funguje rekurze a jaká úskalí nás s ní mohou potkat. Procvičíme si také trochu logického myšlení a zopakujeme to, co jsme si ukazovali v dílech minulých.

V tomto díle seriálu o programování her si napíšeme jednoduchou hru Bubble Breaker. Princip je jednoduchý - máte hrací pole 11 x 11 kuliček různých barev (celkem je jich 5 - červená, žlutá, zelená, modrá a fialová) a vaším úkolem je odstraňovat skupiny kuliček stejné barvy. Pokud tedy kliknete na kuličku, která sousedí s jednou nebo více kuličkami stejné barvy, celá tato oblast kuliček stejné barvy (sousedství v rozích se nepočítá) se označí a ukáže se počet bodů, kolik získáte, když oblast odbouchnete. Čím více je kuliček pohromadě, tím více je bodů, přičemž je to zařízeno tak, abyste za dvě trojice získali méně bodů než za jednu šestici. Pokud vám daný počet bodů stačí, kliknete do skupiny znovu a kuličky explodují. Vlivem gravitace se kuličky nad nově vzniklými volnými místy propadnou dolů a ihned po propadnutí se všechny kuličky natlačí na pravou stranu, jak to jen nejvíc jde. Pokud takto vznikne nalevo prázdný sloupec, vygeneruje se na do tohoto sloupce náhodné vysoký nový sloupec a opět se "natlačí" doprava. Hra končí, pokud již nejsou v hracím poli žádné dvě sousední kuličky stejné barvy. Vaším úkolem je samozřejmě udělat co nejvíce co největších skupin, abyste měli co nejvíc bodů. Pro představu jsou zde ilustrační obrázky:

Ukázka hry Bubble Breaker

Úvodní nastavení formuláře

Vytvořte si ve Visual Basic .NET nový projekt (Windows Application) a formuláři nastavte vlastnost DoubleBuffered na hodnotu True. Tím zamezíme problikávání při vykreslování. Dále nastavte barvu pozadí formuláře na černou nejčernější, takže Color.Black. Aby formulář nešel roztahovat, nastavte mu FormBorderStyle na hodnotu FixedSingle a maximalizaci zakažte nastavením MaximizeButton na hodnotu False. Nakonec na formulář poklepejte a otevře se nám procedura Form1_Load, která se spustí při vytváření formuláře. Ještě před jeho zobrazením musíme vygenerovat nové pole a nastavit velikost klientské oblasti formuláře (tam, kam budeme kreslit) na hodnotu 221x221 pixelů (velikost kuličky budeš 20x20 pixelů, máme pole 11x11 kuliček a 1 pixel je tam kvůli rámečku). Do této procedury tedy vložte tento kód:

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Me.SetClientSizeCore(221, 221)          'velikost vnitřku formuláře na 221x221 pixelů

        Generate()                              'generovat pole
    End Sub

 Pole a jeho generování

Informace o hracím poli si uložíme do dvou oddělených polí. Jednak budeme mít pole p, které bude nést informaci o barvě kuličky, a pole s, které bude nést informaci o tom, jestli je kulička označená, nebo není. Obě pole budou mít velikost 11x11 buněk. Datový typ druhého pole s je jasný - Boolean. Co ale s prvním polem? Vytvoříme si pro něj tzv. Enum, což je seznam pojmenovaných hodnot. Někomu to možná přijde zbytečné, mohli bychom použít normální čísla (třeba 0 = žádná kulička, 1 = červená, 2 = žlutá atd.), ale za půl roku si už asi sotva budeme pamatovat, které číslo měla fialová. Pokud použijeme Enum, vnitřně to bude fungovat stejně, akorát v kódu místo jedničky napíšeme název barvy. Je to tedy hlavně pro nás přehlednější.

    Enum Barva
        None = 0
        Red = 1
        Green = 2
        Yellow = 3
        Blue = 4
        Purple = 5
    End Enum
    Dim p(10, 10) As Barva          'barva na políčku
    Dim s(10, 10) As Boolean        'je políčko označené?

    Dim r As New Random()           'generátor náhodných čísel

    Dim selected As Integer = 0     'počet vybraných kuliček
    Dim score As Integer = 0        'skóre

Vidíme, že fyzicky je pole p opravdu pole čísel, ale místo p(1,2) = 1 napíšeme p(1,2) = Barva.Red. Je to delší, ale Visual Basic .NET nám zobrazí nabídku všech možností, takže stejně většinou stačí stisknout jednu klávesu a zbytek se doplní sám. Všimněte si také, že pole deklaruji (10,10), i když je jejich velikost 11x11. Buňky se v .NET vždy číslují od nuly.

Generování proběhne velmi jednoduše. Nadeklarovali jsme si proměnnou r jako nový objekt Random, což je generátor náhodných čísel. Má metodu Next, které předáme celé číslo N, a tato metoda vrací náhodné číslo od nuly včetně do N - 1 včetně. My musíme generovat čísla od 1 do 5, to znamená, že jako argument předáme 5 (funkce nám tedy vrátí hodnoty od 0 do 4) a přičteme k výsledku jedničku, čímž pádem se trefíme do potřebného intervalu. Takto přiřadíme nějakou barvukaždému políčku ve hře. Úplně nakonec ještě vynulujeme skóre a zobrazíme jej do titulkového pruhu okna.

    Public Sub Generate()
        'vygenerovat hrací pole
        For i As Integer = 0 To 10
            For j As Integer = 0 To 10
                p(i, j) = r.Next(5) + 1     'nastavit náhodné číslo
            Next
        Next
        'vynulovat skóre
        score = 0
        Me.Text = score & " bodů - Bubble Breaker"
    End Sub

Vykreslování kuliček

Je na čase zobrazit vygenerovaný výsledek. Pozadí okna je černé, stačí na něj jen vykreslit kuličky. V okně kódu tedy nahoře do prvního rozbalovacího seznamu nastavte Form1 Events a do druhého Paint. Vytvoří se nám procedura události Paint na formuláři, která zaručí, že se v případě potřeby obsah vykreslí. Dovnitř dáme dvojitý cyklus, který projde všechny buňky v poli a vykreslí potřebné kuličky.  

Pokud bychom ale kuličky vybarvili čistě danou barvou, nevypadalo by to moc hezky. Grafika .NETu nám ovšem umožňuje udělat přechod dvou barev a tím vybarvit nějaký útvar. Jak na to? Vytvoříme objekt Drawing2D.LinearGradientBrush a předáme mu jako argumenty dva body a dvě barvy. V prvním bodě bude první barva, v druhém druhá, a ostatní body se dopočítají podle poměru vzdáleností od obou bodů. Aby kulička dobře vypadala, provedeme to takto:

Průběh přechodu

Čára spojuje dva krajní body přechodu. Vidíme, že první bod je trochu dál od středu než druhý. Přechod totiž nechceme rovnoměrně, kulička musí být více modrá než bílá. Na první bod přiřadíme barvu bílou (Color.Write) a na druhý právě modrou (Color.Blue). Kulička tedy bude vypadat zajímavěji, samotné vykreslení provedeme funkcí FillEllipse, které předáme objekt LinearGradientBrush, čili štětec kreslící přechod barev, a pak souřadnice horního levého rohu opsaného obdélníku a jeho výšku a šířku. Pokud na daném místě kulička není, vybarvíme toto místo černým čtvercem a hned pokračujeme dalším políčkem. Pokud přebarvujeme kuličku jinou barvou, bát se nemusíme, kuličky jsou vždy stejně velké.

První bod štětce budou souřadnice levého horního políčka, od kterých odečteme 5, abychom měli bod posunutý trochu nahoru a doleva. Druhý bod nastavíme na souřadnice pravého dolního rohu pole. První barva bude bílá, druhá bude v proměnné c, kterou jsme určili v rozhodovací struktuře Select Case podle hodnoty v poli p. Pokud je pole prázné, hned v rozhodovací struktuře se vybarví černě a Continue For přeskočí zbytek kódu uvnitř cyklu a pokračuje dalším průchodem.

Zde je tedy procedura vykreslení obsahu okna:

    Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        'vykreslit hrací pole
        With e.Graphics
            For i As Integer = 0 To 10
                For j As Integer = 0 To 10

                    Dim c As Color          'určit barvu podle políčka
                    Select Case p(i, j)
                        Case Barva.Red : c = Color.Red
                        Case Barva.Yellow : c = Color.Yellow
                        Case Barva.Green : c = Color.Green
                        Case Barva.Blue : c = Color.Blue
                        Case Barva.Purple : c = Color.Purple
                        Case Else : .FillRectangle(Brushes.Black, i * 20, j * 20, 20, 20) : Continue For
                    End Select
                    'vykresli kuličku
                    .FillEllipse(New Drawing2D.LinearGradientBrush(New Point(i * 20 - 5, j * 20 - 5), New Point(i * 20 + 20, j * 20 + 20), Color.White, c), i * 20 + 1, j * 20 + 1, 18, 18)
                Next
            Next
        End With
    End Sub

Označení oblasti stejnobarevných kuliček

Pokud chceme označit oblast kuliček stejné barvy, máme několik možností. My se naučíme používat tzv. rekurzi. Je to taková speciální programátorská metoda, která spočívá v zavolání procedury, ve které se aktuálně nacházíme, znovu. Celé to vypadá složitě, chce to ale jen zapnout mozek a trochu zapřemýšlet.

Představte si, že máme proceduru OznacSousedy, které předáme souřadnice políčka. Tato procedura se podívá na okolní políčka a pokud označená nejsou, označí je. Co když ale jedno ze sousedních políček má nějakého souseda, kterého je také třeba označit? A co když je jich více? Můžeme tedy proceduru OznacSousedy pustit i pro každého souseda, kterého najdeme. Protože tato procedura dělá přesně to, co chceme.

Jak na to? Stačí proceduru mírně upravit. Všude, kde se nastavuje, že je políčko vybrané, přidáme i volání procedury pro označení sousedů na toto nově nalezené políčko. Vůbec nevadí, že to je ta samá procedura, kde teď jsme. Stav programu a všechny proměnné se uloží a procedura se spustí znovu od začátku, provede se a v okamžiku, kdy skončí, se obnoví uložený stav a pokračuje se tam, kde se před tím přestalo.

    Public Sub HighLightNeighbours(ByVal x As Integer, ByVal y As Integer)
        'obarvit kuličku i její sousedy, pokud ještě obarveni nejsou
        s(x, y) = True
        selected += 1
        If x > 0 AndAlso p(x, y) = p(x - 1, y) AndAlso Not s(x - 1, y) Then HighLightNeighbours(x - 1, y)
        If x < 10 AndAlso p(x, y) = p(x + 1, y) AndAlso Not s(x + 1, y) Then HighLightNeighbours(x + 1, y)
        If y > 0 AndAlso p(x, y) = p(x, y - 1) AndAlso Not s(x, y - 1) Then HighLightNeighbours(x, y - 1)
        If y < 10 AndAlso p(x, y) = p(x, y + 1) AndAlso Not s(x, y + 1) Then HighLightNeighbours(x, y + 1)
    End Sub

Na následujícím obrázku je rekurze popsána. Všechny tři obdélníky znázorňují tu stejnou proceduru, v kódu je však zapsána pouze jednou. Kód ve všech obdélnících je úplně stejný. Program začíná vstupem do modré procedury. Podmínka se splní, protože vybrané políčko má nějakého souseda stejné barvy, narazíme tedy na rekurzívní volání. Spustíme tedy tu samou proceduru znovu, akorát pro jiné parametry, totiž pro souřadnice nově nalezeného políčka. Opět v ní platí podmínka, protože nové políčko má ještě jednoho neoznačeného souseda. Spustíme tedy žlutou proceduru, opět je to ta samá procedura, akorát má jiné parametry. Její políčko ovšem žádného neoznačeného souseda stejné barvy nemá, proto se podmínka nesplní. Procedura dojde až na konec, k řádku End Sub. Skončí a předá řízení zpět červené proceduře. Ta pokračuje tam, kde přestala, totiž provede další podmínky, popřípadě se opět zavolá, pokud najde jiného souseda, ale jednou zcela jistě skončí a pak předá řízení modré proceduře, která opět pokračuje dál. Je to vlastně stejné, jako když jedna procedura volá jinou, taky počká, než se ta nová provede a pak pokračuje dál. Tady akorát voláme tu samou proceduru.

Rekurze

Teď tedy k tomu, jak funguje kód: Proceduře HighLightNeighbours (vyznač sousedy) při zavolání předáme parametry x a y, což jsou souřadnice nového políčka. Procedura toto políčko označí, přidá jedničku do proměnné obsahující počet označených políček, kterou jsme nadeklarovali na začátku, a pak začne samotné hledání sousedů. Každá podmínka je složená ze tří částí - první část vždy kontroluje, jestli nebudeme sahat mimo pole. Pokud budeme v prvním sloupci, souseda zleva kontrolovat nemusíme (a vlastně ani nesmíme, protože bychom požadovali políčko s indexem -1, které v poli není, a nastala by chyba). To samé platí pro zbývající tři případy.

Další část podmínky kontroluje, jestli soused má stejnou barvu. Pokud nemá, není co řešit, pokračujeme až dalším řádkem. Pokud ano, pokračuje se kontrolou třetí části. Ta je velice důležitá - ověří, jestli políčko, které zkoušíme, již není označené. Pokud by bylo a my přesto rekurzivní proceduru volali, vraceli bychom se pořád na políčka, kde jsme již byli. To by byl pěkný průšvih, protože rekurze by nikdy neskončila a za chvíli by operačnímu systému došla trpělivost a vyhodil by chybu přetečení zásobníku (tam se totiž ukládají hodnoty proměnných a návratové adresy procedur, jenže zásobník není bezedný a za chvíli ho máme plný). Pokud ale políčko označené nebude, máme jistotu, že jsme na něm ještě nebyli. Můžeme jej tedy označit a případně označit i jeho sousedy, pokud je má, o to už se postará naše procedura.

Volání rekurzivní procedury musí být vždy v podmínce, která někdy zcela jistě přestane platit! V našem případě to tak je - určitě někdy natrefíme na políčko, které už nemá žádného neoznačeného souseda stejné barvy. Jinak nám rekurze nikdy neskončí a přeteče zásobník, nemůžeme ji volat donekonečna. Pokud jste rekurzi nepochopili, nevadí, zkuste nad tím trochu zapřemýšlet, pokud stále na nic nepřijdete, napište do fóra nebo to nechte plavat, určitě to časem pochopíte. Rekurze je již velmi pokročilá technika a je dosti ošemetná, snadno se v ní udělá chyba. A ještě k tomu se špatně vysvětluje.

Také jste si možná všimli, že jsem místo operátoru And použil AndAlso. To má také svůj význam. Pokud použijeme AndAlso a první podmínka se nesplní, druhá se už testovat nebude. To je v tomto případě nutné, protože pokud by se testovala, vyskočili bychom z pole a nastala by chyba. Pokud máme samotné And, vyhodnotí se obě části a výsledek se určí až na konec. Obdobně jsou na tom operátory Or a OrElse. Pokud použijeme OrElse a první podmínka platí, druhá se už také testovat nebude.

Znázornění vybrané oblasti

Když na kuličky klikneme, je třeba, aby se označená oblast obtáhla. To však v tomto případě není složité. Při procházení kuliček ve vykreslovací proceduře dáme podmínku a pokud je aktuálně vykreslovaná kulička označená, budeme vykreslovat ohraničení oblasti. Ovšem jen na těch stranách, na kterých sousední kulička nemá stejnou barvu. To je totiž zaručeně kraj této oblasti. Pokud je napravo od nás kulička stejné barvy, hranici na pravou stranu kreslit nemůžeme. Pokud je tam ale jiná barva, hranice tam zcela určitě bude. Opět nesmíme zapomenout nejdříve zkontrolovat případy, kdy jsme na kraji pole a sahali bychom na neexistující indexy. Pokud dané podmínky přidáme do procedury vykreslování, bude vypadat takto:

    Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        'vykreslit hrací pole
        With e.Graphics
            For i As Integer = 0 To 10
                For j As Integer = 0 To 10

                    Dim c As Color          'určit barvu podle políčka
                    Select Case p(i, j)
                        Case Barva.Red : c = Color.Red
                        Case Barva.Yellow : c = Color.Yellow
                        Case Barva.Green : c = Color.Green
                        Case Barva.Blue : c = Color.Blue
                        Case Barva.Purple : c = Color.Purple
                        Case Else : .FillRectangle(Brushes.Black, i * 20, j * 20, 20, 20) : Continue For
                    End Select
                    'vykresli kuličku
                    .FillEllipse(New Drawing2D.LinearGradientBrush(New Point(i * 20 - 5, j * 20 - 5), New Point(i * 20 + 20, j * 20 + 20), Color.White, c), i * 20 + 1, j * 20 + 1, 18, 18)

                    If s(i, j) Then     'pokud je kulička označená a sousední nemá stejnou barvu, vykreslit hraniční čáry oblasti
                        If i > 0 AndAlso p(i, j) <> p(i - 1, j) Then .DrawLine(Pens.White, i * 20, j * 20, i * 20, j * 20 + 20)
                        If i < 10 AndAlso p(i, j) <> p(i + 1, j) Then .DrawLine(Pens.White, i * 20 + 20, j * 20, i * 20 + 20, j * 20 + 20)
                        If j > 0 AndAlso p(i, j) <> p(i, j - 1) Then .DrawLine(Pens.White, i * 20, j * 20, i * 20 + 20, j * 20)
                        If j < 10 AndAlso p(i, j) <> p(i, j + 1) Then .DrawLine(Pens.White, i * 20, j * 20 + 20, i * 20 + 20, j * 20 + 20)
                    End If
                Next
            Next
        End With
    End Sub

Výbuch a posuny kuliček

Pokud na označenou skupinu klikneme ještě jednou, kuličky vybuchnou a místo nich se na chviličku zobrazí prázdné mezery. To je velmi jednoduché - zkrátka tam, kde je políčko označené, nastavíme jako barvu Barva.None a je to.

    Public Sub Explode()
        'odprásknout všechny vybrané kuličky
        For x As Integer = 0 To 10
            For y As Integer = 0 To 10
                If s(x, y) Then p(x, y) = Barva.None
            Next
        Next
        Me.Invalidate() : Application.DoEvents() : Threading.Thread.Sleep(50)
    End Sub

Zajímavé je zde snad jen to, že po skončení cyklu zavoláme Me.Invalidate, což řekne systému, že je třeba co nejdříve znovu vykreslit celou plochu, že se na ní asi něco změnilo. Důležité je ale to, že se to neprovede hned, ale až když má systém čas. Proto mu jej přidělíme zavoláním Application.DoEvents, které pozastaví na chvíli naši aplikaci (v řádu zlomků sekundy, nic dlouhého) a dá systému prostor, aby si vyřešil to, co má. Provede se tedy mimo jiné překreslení formuláře. Poslední příkaz Threading.Thread.Sleep(50) počká ještě 50 milisekund, aby ve hře byl alespoň náznak nějaké jednoduché animace.

Po výbuchu nejprve kuličky vlivem gravitace spadnou dolů. To vyřešíme jednoduše. Použijeme opět dvojitý cyklus, akorát trochu složitější. Vnější cyklus projde sloupce a s každým sloupcem zvlášť provede gravitační spád. Jak tento spád provést? Jednoduše. Uděláme si tzv. kurzor - proměnnou, která si bude pamatovat pozici, kam se má posunout následující kulička. Nastavíme jej na 10, tedy na poslední řádek, nejspodnější místo. A spustíme cyklus, který projde sloupce zespoda nahoru (protože obracíme pořadí, musíme zapsat For i = 10 to 0 Step -1). Pokud narazíme v tomto sloupci na kuličku, přesuneme ji na místo kurzoru a tento kurzor posuneme o jednu pozici nahoru. Jakmile v cyklu dojdeme na konec řádku, vyčistíme všechny pozice od kurzoru nahoru, aby nám tam nezůstaly kuličky, zkrátka je nastavíme na Barva.None.

Pokud budete mít čáru a po ní rozházeno náhodně několik míčů, jeden člověk projde celou řadu a když narazí na míč, hodí jej druhému, který stojí na začátku. Ten míč položí na zem a udělá krok dopředu. A jak mu házíte další a další míče a on pořád postupuje dopředu, za chvíli máte míče v řadě hned vedle sebe. Když jsou míče seřazení, člověk, který chytal, doběhne na konec čáry za tím prvním. A přesně takhle funguje i naše procedura. Kontrolní otázka - který z nich je kurzor? První nebo druhý? Jistěže ten druhý.

    Public Sub MoveDown()
        'posune kuličky dolů
        For x As Integer = 0 To 10
            Dim t As Integer = 10
            For y As Integer = 10 To 0 Step -1
                If p(x, y) > 0 Then p(x, t) = p(x, y) : t -= 1
            Next
            For y As Integer = t To 0 Step -1
                p(x, y) = Barva.None
            Next
        Next
        Me.Invalidate() : Application.DoEvents() : Threading.Thread.Sleep(50)
    End Sub

Kuličky tedy po výbuchu spadnou. Nyní se musí ještě posunout doprava, co nejvíce to jde. To je naprosto identický případ, akorát prohodíme souřadnice - spád probíhá v řádcích a ne ve sloupcích. Pokud se tím uvolní celý poslední sloupec (stačí když je volné políčko v dolním levém rohu, nad ním nic být nemůže, gravitací to muselo spadnout), vygenerujeme náhodně vysoký sloupec hned na levý kraj a zavoláme posunutí doprava znovu, aby se kuličky zařadily. To je vlastně také rekurze, skončí ve chvíli, kdy poslední sloupec volný nebude, což se jednou určtiě stane.

    Public Sub MoveRight()
        'posune kuličky doprava
        For y As Integer = 0 To 10
            Dim t As Integer = 10
            For x As Integer = 10 To 0 Step -1
                If p(x, y) > 0 Then p(t, y) = p(x, y) : t -= 1
            Next
            For x As Integer = t To 0 Step -1
                p(x, y) = 0
            Next
        Next
        Me.Invalidate() : Application.DoEvents() : Threading.Thread.Sleep(50)

        'pokud je první sloupec prázdný, dogenerovat nový
        If p(0, 10) = 0 Then
            For y As Integer = 10 To r.Next(10) Step -1
                p(0, y) = r.Next(5) + 1
            Next
            Me.Invalidate() : Application.DoEvents() : Threading.Thread.Sleep(50)
            MoveRight()
        End If
    End Sub

Samozřejmě opět si necháváme pauzu, aby to vypadalo animovaně, výsledný efekt je však dostatečný. Intervaly nejsou příliš dlouhé, takže to hráče nezdržuje.

Ostatní obslužné funkce

Ještě než napíšeme samotné ovládání kliknutím, musíme vytvořit ještě několik funkcí, kterými si ulehčíme práci. Určitě se bude hodit funkce ClearSelection, která všem políčkům nastaví, že nejsou onačené. Adekvátně také nastaví hodnotu proměnné selected, která obsahuje počet označených kuliček.

    Public Sub ClearSelection()
        'smazat výběr oblasti
        For i As Integer = 0 To 10
            For j As Integer = 0 To 10
                s(i, j) = False
            Next
        Next
        selected = 0
        HideLabel()
    End Sub

Vidíme, že tato procedura volá ještě jakousi proceduru HideLabel. Hned ji napíšeme. Abychom totiž mohli snadno u vybrané oblasti zobrazit, kolik bodů za ni bude, přidáme na formulář komponentu Label. Barvu pozadí jí nastavíme na bílou a barvu textu na černou. Vymažeme z ní veškerý text a nastavíme jí Visible na False. Procedura HideLabel tuto komponentu schová a musíme napsat ještě proceduru SetLabel, které předáme souřadnice políčka a počet bodů, a tato procedura umístí Label někam k danému políčku, zobrazí ho a vypíše do něj počet bodů. Při nastavování pozice komponenty Label ještě kontrolujeme, jestli již není za okrajem okna. Pokud je políčko v pravé polovině okna, posuneme ji o dvě políčka směrem dolva. Podobně je to i svisle. Obě procedury jsou zde:

    Public Sub SetLabel(ByVal text As String, ByVal x As Integer, ByVal y As Integer)
        Label1.Text = text
        Label1.Left = x * 20 + 20 : If Label1.Left > 110 Then Label1.Left -= 40
        Label1.Top = y * 20 + 20 : If Label1.Top > 110 Then Label1.Top -= 40
        Label1.Visible = True
    End Sub
    Public Sub HideLabel()
        Label1.Visible = False
    End Sub

Dále se nám bude hodit procedura GameOver, která Label zobrazí, roztáhne na celou plochu formuláře, napíše do něj text Konec hry, počká 2 sekundy a nakonec vrátí Label do původního stavu.

    Public Sub GameOver()
        'konec hry
        Label1.Text = "KONEC HRY"
        Label1.Visible = True
        Label1.AutoSize = False
        Label1.Location = New Point(0, 0)
        Label1.Size = Me.ClientSize

        'počkat 2 sekundy
        Application.DoEvents()
        Threading.Thread.Sleep(2000)

        'vygenerovat novou hru a vrátit Label do původního stavu
        Label1.Visible = False
        Label1.AutoSize = True
        Generate()
    End Sub

Komponentě Label, kterou jste vytvořili na formuláři, ještě nastavte hodnotu vlastnosti TextAlign na hodnotu MiddleCenter, tedy úplně doprostřed (svisle i vodorovně). To proto, aby se text Konec hry zobrazil na středu formuláře.

A poslední funkce, která nám bude zjišťovat, jestli hra už neskončila, bude funkce HasPossibilities. Projde celé pole a pokud najde dvě stejné barvy vedle sebe nebo nad sebou (opět s kontrolou, abychom nevyskočili z pole), vrátí True. Pokud nic takového nenajde, vrátí False. V takovém případě hra končí, hráč již nemá žádnou skupinu, kterou by mohl odstřelit.

    Public Function HasPossibilities() As Boolean
        'spočítat, jestli máme nějakou možnost
        For x As Integer = 0 To 10
            For y As Integer = 0 To 10
                If p(x, y) > 0 Then
                    If x < 10 AndAlso p(x, y) = p(x + 1, y) Then Return True
                    If y < 10 AndAlso p(x, y) = p(x, y + 1) Then Return True
                End If
            Next
        Next
        Return False
    End Function

Ovládání

Pokud jste hru zkoušeli spustit, až dosud se nic moc nedělo. Touto poslední procedurou ale vše napravím a hra bude kompletní. Nalistujte proceduru Form1_MouseDown, která se spustí, pokud uživatel klikne myší (v okamžiku, kdy tlačítko jede dolů). A vložte do ní tento kód:

    Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseDown
        'obtáhnout kuličky stejné barvy
        Dim x As Integer = e.X \ 20
        Dim y As Integer = e.Y \ 20

        If s(x, y) And selected > 1 Then
            'odkliknout oblast a posunout kuličky
            score += CInt(Label1.Text)
            Me.Text = score & " bodů - Bubble Breaker"
            Explode()
            ClearSelection()
            MoveDown()
            MoveRight()
            If Not HasPossibilities() Then GameOver()

        Else
            'vybrat oblast
            ClearSelection()
            If p(x, y) > 0 Then HighLightNeighbours(x, y)
            Me.Invalidate()
            If selected > 1 Then SetLabel(selected * (selected - 1), x, y)

        End If
    End Sub

Nejprve podle souřadnice myši zjistíme souřadnice políčka (vydělením jeho velikostí a zaokrouhlením dolů). Pokud jsme kliknuli na políčko, které je vybráno, a počet vybraných kuliček je větší než 1 (je vybrána skupina a ne samostatné políčko), přičteme spočítané skóre z komponenty Label k celkovému skóre a vypíšeme jej do titulku okna. Dále provedeme explozi, odznačíme všechny kuličky, provedeme gravitační spád a posun doprava s případným doplněním sloupců, to jsme již vše napsali a víme, jak to funguje. Pokud již nezbývají žádné možnosti, ukončíme hru. Pokud jsme kliknuli na neoznačené políčko, opět odznačíme všechny kuličky a zavoláme rekurzivní proceduru pro označení celé stejnobarevné oblasti. Pokud má oblast více než 1 kuličku, zobrazíme Label s počtem bodů za tuto oblast. Počet bodů spočítáme podle vzorce skóre = n . (n - 1), kde n je počet vybraných kuliček, tedy selected. Tento vzorec je navržen tak, aby bylo výhodnější udělat jednu větší oblast než dvě menší.

A to je celá hra. Pokud jste jakoukoliv pasáž nepochopili, ptejte se ve fóru, rád zodpovím případné dotazy.

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Všechny díly tohoto seriálu

3. Bubble Breaker 01.06.2007
2. Hungry Snake 2 26.04.2007
1. Hungry Snake 1 25.04.2007

 

Mohlo by vás také zajímat

Windows Presentation Foundation (WPF) - díl 4.: Architektura a objektový model WPF

Na jaké kompromisy museli architekti WPF frameworku přistoupit, aby nabídli vývojářům pohodlný vývoj ve vyšších programovacích jazycích a zároveň odpovídající výkon výsledného uživatelského prostředí? Tento článek se věnuje architektuře WPF frameworku.

Windows Presentation Foundation (WPF) - díl 6.: Základy pozicování

Pozicování je velká přednost technologie WPF. Dovoluje připravovat dynamické rozložení prvků s předvídatelným chováním při změně nejen velikosti okna, ale i elementů uvnitř něj. V tomto díle se věnuji základním principům pozicování.

Práce s časovými pásmy a letním časem v aplikaci a databázi - díl 3.: DateTimeOffset v .NET Frameworku

DateTimeOffset je méně využívanou alternativou struktury DateTime v .NET Frameworku. Navíc dovoluje ukládat časové pásmo a pohodlně s ním pracovat.

 

 

Nový příspěvek

 

Diskuse: Bubble Breaker

Každopádně bych moc rád poděkoval za zápočet a zkoušku, kterou doufám díky tohoto článku splním.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: Bubble Breaker

Ahoj. Studuji střední školu a mym oborem jsou zrovna počítače. A dostal jsem za ukol naprogramovat takovou jednoduchouckou hru. Vím jak na to jenom nevím jak to napsat. Nemohl by jste mi prosim poradit kdyýžtak na mail [email protected] Dekuju.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

A to jako čekáte, že někdo vám bude řešit domácí úkoly?

Pokud máte konkrétní dotaz, máte kus kódu, který nefunguje a potřebujete poradit s nějakou maličkostí, rádi poradíme. Ale pokud po nás chcete, abychm řešili vaši neschopnost nebo lenost dělat si svou práci a své úkoly, tak za tohle se tu posílá minimálně do háje, ne-li dále.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Já nechci vyresit ukol ja jen potrebuji radu ;) potrebuji hodit kostkou. Tzn...aby mi program nahodne vybral jedno cislo z intervalu 1-6 ;)

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Tak založte nový dotaz ve fóru. To se netýká článku o hře Bubble Breaker.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: Bubble Breaker

Zdravím,

pěkný návod na hru ve VB. Já se zabývám VB v Excelu a hra se mi tak líbila,že jsem se rozhodl ji přepracovat do Excelu. Výsledek ji najdete zde http://www.davidfranc.wz.cz/excel/Bubleb...

David

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Diskuse: Bubble Breaker

Pls mohl by jsi mi hru poslat na mail [email protected] ??? ja ji vsude shanim a nemuzu ji sehnat.... a je v ni megashift?

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Diskuse: Bubble Breaker

Zdar chtel bych se te zeptat jestli nevis jak mohu udelat hru 15.

Pleas napiš mi¨jak na ni.

Předem dík za odpoved na e-mail [email protected]

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Diskuse: Bubble Breaker

akorat dve vyhrady bych mel:

1. zadny objektovy navrh: ja bych logiku rozdelil minimalne mezi dve tridy, jednu (Control) pro hraci plochu, druhou pro samostatne kulicky. dalsi veci, ktera by pripadne stala za vytazeni do vlastni tridy, by byla logika "smrskavani" kulicek po explozi.

taky mi prijde zbytecny definovat nejaky enum Barva, a pak ho slozite mapovat na standardni System.Drawing.Color. Proc nepouzit Color rovnou?

2. vylozena prasarna je sleepovani UI threadu a DoEvents. mnohem cistsi reseni animace by bylo pomoci System.Windows.Forms.Timeru, ktery by se zapinal po dobu animace.

jestli budu mit nekdy cas, zkusim napsat C# verzi

nahlásit spamnahlásit spam -2 / 2 odpovědětodpovědět

a dalsi vec, ktere jsem si vsimnul: brush se pokazde vytvari znovu, a co je horsi, nikdy se nelikviduje (metoda Dispose).

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Tento seriál je koncipován pro mírně pokročilé uživatele, ne pro experty (je pravda, že to z článku příliš nevyplývá). Objektový návrh je sice věc pěkná, ale pro potřeby tak jednoduché hry je to takříkajíc "kanón na vrabce". Je evidentní, že by bylo zcela vhodné vytvořit třídy, ale na druhou stranu mírně pokročilí uživatelé potřebují procvičit především pole a rozhodně jsem se nechtěl pouštět do objektů, to až snad někdy příště.

Nicméně seriál pro pokročilé o objektově orientovaném programování udělat mohu. Do takto primitivní hry ovšem rozhodně nemá cenu plácat objekty, protože si práci akorát přiděláme. Do věcí složitějších, kde objekty pomohou, to samozřejmě smysl mít bude.

Enum jsem také použil spíše z didaktických účelů, uznávám, že to není úplně nejlepší příklad jeho využití. Volání Dispose na Brush by tam samozřejmě být mělo, omlouvám se a doplním. Brush se ovšem vytvářet pokaždé znovu musí, protože je určen pro jiné barvy kuličky a pro jiné body. Samozřejmě řešením by bylo předkreslit si kuličky do paměti a pak používat pouze DrawImage, bylo by to i rychlejší.

A co se týče DoEvents a Thread.Sleep, prasárna to není, jsou to standardní metody používané k pozastavení běhu aplikace. Komponenta Timer je poměrně nepřesná a na krátké časové intervaly nevhodná, takže bych její použití rozhodně nedoporučoval (btw jak asi myslíte, že funguje uvnitř, stejně jako Thread.Sleep). Thread.Sleep je čistý protředek pro pozastavení běhu aplikace na krátkou dobu bez zakládání nového vlákna, což by opět situaci komplikovalo.

Nicméně děkuji za připomínky.

nahlásit spamnahlásit spam 6 / 6 odpovědětodpovědět

nojo, mas pravdu ze v serialu pro zacatecniky je OOP "balast" navic. jenze pak rostou programatori, kteri se OOP nenauci ani pozdeji, a to je pruser. ja se snazim zacatecniky od pocatku nevest k hledani "nejmensi oddelitelny jednotky" nejen na urovni metod, ale i trid. i kdyz je pravda ze vysvetlit dalsi klicovy slova je problem.

muze to byt dobre tema na dalsi dil, ukazat jak stavajici funkcionalitu refaktorovat do usercontroly .

co se tyce timeru, pozor na to: v .netu jsou minimalne dve tridy timer. jednak

- System.Threading.Timer, ktera funguje tak, ze na dedikovanem (poolovem) vlakne vola zadany callback delegat, tudiz je naprosto nevhodna pro toto pouziti, jelikoz vsechny User32 a GDI32 objekty (okna, stetce, device contexty, ...) lze pouzivat pouze na vlakne ktere je vytvorilo (UI threadu)

- ale System.Windows.Forms.Timer funguje uplne jinak, nebot vevnitr vola Win32 funkci SetTimer. ta zpusobi ze do fronty zprav je v urcitych intervalech zaslana zprava WM_TIMER a ta se zpracuje stejne jako kazda jina. tudiz nikde se zadny Sleep nevola, a UI thread je volny pro obsluhu jinych udalosti (kliknuti na tlacitko, prekreslovani, etc).

je pravda, ze WM_TIMER zpravy maji nizsi prioritu nez ostatni, tudiz teoreticky muze dojit ke zdrzeni.

ale myslim si, ze v tomto pripade to bude jen tezko postrehnutelne. pokud by to vadilo, spravne reseni je prepsat animujici algoritmus tak, aby nepracoval inkrementalne, ale aby si pamatoval kdy byla animace odstartovana, a vzdalenost pocital z uplnyuteho casu misto konstantniho prirustku. (neboli implementovat frame dropping)

kazdopadne si stojim za tim, ze sleepovat UI thread (nebo na nem delat synchronni I/O) je prasarna nejvetsiho kalibru, a DoEvents je hnusny workaround, zdedeny z VB6 (kde opravdicky multithreading nebyl), ktery by bylo radno zapomenout a nikdy nepouzit, protoze vzdy to lze vyresit spravne.

nahlásit spamnahlásit spam -2 / 2 odpovědětodpovědět

O OOP určitě něco napíšu, ale začátečníkům s tím plést hlavu opravdu nebudu. Je pravda, že mnoho lidí se naučí cykly, pole a procedury a myslí si, že už umí programovat. Je ovšem jasné, že OOP je potřeba, nicméně jej opravdu nemá smysl cpát všude, zvláště pak do takto jednoduchého příkladu. Stejně tak frame dropping, je to dost pokročilá technika, kterou bych začátečníky popletl.

S tím Timerem máte pravdu a uznávám, že není korektní používat Thread.Sleep. Na druhou stranu my vlastně potřebujeme, aby se aplikace "zamknula" a nepřijímala události klikání, když probíhá animace. Kvůli překreslení se volá DoEvents, které pochází ze starého Visual Basicu, ale je i v .NETu a své místo tam rozhodně má.

Uznávám, že v tomto článku nepoužívám vždy ty nejlepší postupy, ovšem má to své opodstatnění.

Naučit se programovat zvládne leckdo, ale programátorů chybí v ČR tisíce. Moc lidí to nechce dělat - nebaví je to. Proto se snažím, abych svými články zájemce o tento obor neotrávil. Mohl jsem napsat, že se dá použít řešení na jeden řádek, ale uděláme to přes Timer s frame droppingem, protože to tak je lepší a má se to tak dělat. Ale tři čtvrtiny začátečníků pak článek nepochopí. Alespoň ze začátku je potřeba motivovat - začátečník potřebuje vidět výsledek a je jasné, že to hned nebude napsané, jako kdyby to dělal zkušený programátor. Proto sem raději netahám objekty a Timer - je to víc kódu. Řešení na jeden řádek přes Thread.Sleep funguje také (a navíc je záhodno, aby během animace nešly události, protože nemůžeme klikat, dokud neskončí animace, byly by v tom chyby, i to by se dalo ošteřit, ale už je to další komplikace).

I když to není nejideálnější způsob hodný zkušeného a ostříleného programátora, začátečník si procvičí pole a kreslení přes GDI+ (nedělám si iluze, že více než polovina začátečníků vůbec pochopí rekurzi). Pokud ho programování chytne, napíše si pár menších her a sám zjistí, že na složitější věci jsou potřeba objekty a že Thread.Sleep není to pravé ořechové. Anebo se to někde dočte, třeba zde v diskusi. Ale z vlastní zkušenosti i z kurzů programování, které vedu, vím, že je ze začátku lepší říci méně a ukázat to nejjednodušší a nejkratší možné řešení (neberte to doslova, ale v 90% případů to tak je). Ten, koho programování začne bavit, v budoucnu mít problém s naučením se těch správných postupů nebude. Pokud bych napsal komplexní článek, jehož výsledkem sice bude perfektní aplikace tak, jak se na profesionála sluší a patří, po jeho přečtení nezačne programování bavit nikoho.

Ale i tak děkuji za reakci, až budu mít čas, napíšu o psaní opravdových her tak, jak mají být. V současnosti ovšem pracuji na vylepšeních tohoto webu, takže času na články moc není.

nahlásit spamnahlásit spam 7 / 7 odpovědětodpovědět

Já jako začátečník (tedy alespoň co se týče VB.NET) jsem za tento článek nesmírně rád. Zase jsem se naučil pár nových věcí. Také držím palce a těším se na další článečky ;-). Třeba někdy taky napíšu nějakou svojí hru. Už delší dobu si brousím zuby na piškvorky tak si snad najdu nějakou pauzu mezi lenošením a "nicneděláním" a vrhnu se na to :-)

nahlásit spamnahlásit spam 7 / 7 odpovědětodpovědět

Nechápu Vás, proč hledáte chyby tam kde nejsou? Komu vadí, že aplikace na pár desítek milisekund vytuhne? Samozřejmně, že jdou věci řešit jinak a čistěji, ale proč motat začátečníkům hlavu? Pokud se budou vysvětlovat vlákna, tak na něčem, kde je jejich využití jasné.

nahlásit spamnahlásit spam -3 / 3 odpovědětodpovědět

Souhlasim s vami temer se vsim, jedinou vytku mam k tomu ze "sleepovat UI thread je prasarna nejvetsiho kalibru" a ze "DoEvents je hnusny workaround, zdedeny z VB6". Zaprve existuji vetsi prasarny se kteryi se muzete casto setkat a za druhe DoEvents mozna neni ideani na graficke animace, nicmene pokud treba mam cyklus ktery je narocny na vypocet ale presto chci zobrazovat jeho prubeh, tak do smycky dam doevents prave pro to aby neumrtvil cele okno a nenaskakovalo potom takove to zname "Project1 (neodpovídá)" Doevents proste da prostor dalsim uloham a diky tomu si i udela cas na prokresleni okna...

nahlásit spamnahlásit spam -3 / 3 odpovědětodpovědět

Diskuse: Bubble Breaker

Dobry den, potreboval by som poradit, ako by sa dala v GDI+ spravit jednoducha animacia bez preblikavania ? Dajme tomu vykreslim na PictureBox nejake nahodne pozadie (ciary, elipsy..) a chcel by som aby z jedneho rohu do druheho presiel po stlaceni tlacitka polo-transparentny kruh.

Vykreslovat cele pozadie znova a kruh na inom mieste posunuty o pixel je blbost : )

Problem je asi ten, ci existuje funkcia, ktora by mi ulozila cast toho co uz bolo nakreslene (region), nakreslila ten kruh, pockala a znova ten kruh vymazala. (prekreslila na nho povodny region)

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Diskuse: Bubble Breaker

Ahoj Tomáš!

Nedá mi to aby som nereagoval na túto kritiku. Verím, že ju robí človek, ktorý tomu naozaj rozumie. Rád by som sa pozrel na jeho stránky, ktoré ponúka začiatočníkom,. Zatiaľ som ich nenašiel, a preto som presvedčený, že to čo píšeš na stránkach zrozumiteľných čechom a slovákom je zatiaľ najlepšie. Prosím Ťa, v žiadnom prípade sa nedaj znechutiť. Aj keď, musím priznať, že kritika od ľudí, ktorí tomu rozumejú je dobrá vec.

Takže Vám obom držím palce a prajem veľa úspechov.

Ľubo.

nahlásit spamnahlásit spam 3 / 3 odpovědětodpovědět

Já jsem za tuto kritiku docela rád - aby člověk dělal věci dobře, je potřeba znát názory ostatních. Ten člověk .NETu nepochybně dobře rozumí, ale já si stojím za tím, že článek musí být především srozumitelný pro začátečníky, nemá cenu vysvětlovat začátečníkům nebo mírně pokročilým "to správné řešení", pokud je výrazně složitější a výsledek je stejný.

Každý z nás někdy s programováním začínal a přiznejme si, že i moje první aplikace nebyly napsané tím správným způsobem. Až časem a zkušenostmi člověk pozná, že správné řešení má mnoho výhod a že se je vyplatí používat, protože si ušetří mnoho práce i času při rozšiřování aplikace. Ale za kritiku jsem rád, nikdo není dokonalý. Respektuji názory ostatních.

nahlásit spamnahlásit spam 2 / 2 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback