SQL týden Geek tričko zdarma
Tričko zdarma! Stačí před dobitím bodů použít kód TRIKO15. Více informací zde
Do 19.5. až 80% sleva na Kurzy SQL.

Lekce 15 - Hra tetris v MonoGame: Skóre

Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem.
Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulém dílu seriálu MonoGame tutoriálů, MonoGame: Skrolující text (autoři hry), jsme si vytvořili rolující text s autory. Dnes hru obohatíme o tabulku skóre, která bude, jak máme slíbeno, internetová. Tak směle do toho.

Komponenta skóre tabulky

Tabulka skóre bude komponenta. A jak mnozí tuší, tak do složky Komponenty/ vytvoříme třídu s názvem KomponentaSkoreTabulka a podědíme ji z DrawableGameComponent. Dále přidáme privátní proměnné pro Hra a Texture2D s názvem pozadi. V LoadContent() texturu načteme. Dosud vše známé:

public class KomponentaSkoreTabulka : DrawableGameComponent
{
    private Hra hra;
    private Texture2D pozadi;

    public KomponentaSkoreTabulka(Hra hra) : base(hra)
    {
        this.hra = hra;
    }

    protected override void LoadContent()
    {
        pozadi = hra.Content.Load<Texture2D>(@"Sprity\pozadi_menu");
        base.LoadContent();
    }
}

Smyslem této komponenty bude ukládání a zobrazování dosažených výsledků v naší hře. Po skončení hry kolizí nově vytvořené kostky se hra přepne na obrazovku skóre, kde zobrazí uložené výsledky a vyzve aktuálního hráče k uložení výsledku pod přezdívku. K tomu budeme potřebovat ještě pár proměnných: řetězec pro přezdívku a kolekci uložených výsledků (rekordů). K tomu máme třídu Hrac. A protože výsledků může být na více stránek, tak si budeme ještě držet aktuální stánku. Tyto proměnné do třídy KomponentaSkoreTabulka přidáme:

private int strana;
private List<Hrac> hraci;
private string prezdivka;

Skóre klient

Výsledky budeme ukládat na server a následně stahovat do souboru. Proto vytvoříme speciální třídu SkoreKlient, která bude zajišťovat ukládání a načítání dat skóre tabulky. V komponentě budeme pouze volat její metody, jmenovitě Nacti() a Uloz(), případně si zde získáme text chybových hlášek. Tuto třídu si zatím jen připravme, doprogramujeme ji příště:

public class SkoreKlient
{

        public string ChybaNacitani => $"Skóre se nepodařilo načíst.\n Soubor '{soubor}' pravděpodobně neexistuje";
        public string ChybaUkladani => $"Skóre se nepodařilo uložit.\n Vyskytla se chyba při ukládání souboru.";

        private readonly string soubor = "skore.xml";

        public void Uloz(Hrac hrac)
        {
        }

        public List<Hrac> Nacti()
        {
        }
}

Rovnou jsme si připravili i název souboru, do kterého příště skóre z internetu stáhneme.

Vrátíme se zpět do třídy KomponentaSkoreTabulka a instanci SkoreKlient přidáme mezi její privátní atributy:

private SkoreKlient klient;

A v neposlední řadě nezapomeneme, vše co jsme dosud deklarovali, inicializovat:

public override void Initialize()
{
    hraci = new List<Hrac>();
    strana = 0;
    prezdivka = "";
    klient = new SkoreKlient();
    base.Initialize();
}

Stav skóre tabulky

Odchytávání chyb při načítání nebo ukládání skóre je celkem ošemetná záležitost zvláště pak v aplikacích využívající update-render smyčku. My to vyřešíme fikaně pomocí enum stavů tabulky a konstrukce switch.

Prvně si nadefinujeme enum eStav a vytvořené enum použijeme jako typ pro proměnou stav:

private enum eStav
{
    ChybaNacteni,
    ChybaUlozeni,
    Zapis,
    Vypis,
    Otazka,
}

private eStav stav;

Skóre tabulka může nyní buď ukazovat chybu načtení, ukazovat chybu uložení, zapisovat nový rekord nebo vypisovat tabulku rekordů. A co hodnota Otazka? Tu použijeme v případě, že se aplikace uživatele ptá, zda si přeje uložit své dosažené skóre.

Uložení a načtení skóre tabulky

Stav budou měnit zejména metody Uloz() a Nacti(). Využijeme v nich try-catch blok při volání metod klienta, kterého jsme si připravili výše. Stav změníme pokud vše projde nebo i pokud byla vyhozena výjimka.

public void Uloz()
{
    try
    {
        klient.Uloz(hra.hrac);
        Nacti();
        stav = eStav.Vypis;
    }
    catch
    {
        stav = eStav.ChybaUlozeni;
    }
}

public void Nacti()
{
    stav = eStav.Vypis;
    try
    {
        klient.Stahni();
        hraci = klient.Nacti();
    }
    catch
    {
        stav = eStav.ChybaNacteni;
    }
}

Draw()

Teď ještě napsat metody Update() a Draw(). Začneme metodou Draw():

public override void Draw(GameTime gameTime)
{
    hra.spriteBatch.Begin();
    hra.spriteBatch.Draw(pozadi, new Vector2(0, 0), Color.White);

    // výběr vykreslování dle aktuálního stavu
    switch (stav)
    {
        // vykreslení dotazu na nahrání skóre
        case eStav.Otazka:
            hra.spriteBatch.TextSeStinem(hra.fontCourierNew, "Přejete si uložit skóre ?\n\n" +
                                                     "[A/N]", new Vector2(510, 240), Color.Red);
            break;
        // vykreslení zadávání přezdívky
        case eStav.Zapis:
            hra.spriteBatch.TextSeStinem(hra.fontCourierNew, "Zadejte jméno:", new Vector2(510, 240), Color.Red);
            hra.spriteBatch.DrawString(hra.fontBlox, prezdivka, new Vector2(510, 280), Color.Yellow);
            break;
        // výpis hráčů v tabulce
        case eStav.Vypis:
            // index posledního hráče v tabulce
            int posledni = (strana * 12) + 11;
            if (posledni > hraci.Count - 1)
                posledni = hraci.Count - 1;
            // index prvního hráče v tabulce
            int prvni = strana * 12;
            // výpis aktuální strany tabulky
            for (int i = prvni; i <= posledni; i++)
            {
                string text = (i + 1).ToString().PadLeft(3) + ". " + hraci[i].prezdivka + ":" + hraci[i].body;
                hra.spriteBatch.TextSeStinem(hra.fontCourierNew, text, new Vector2(510, 240 + (i - prvni) * 28), Color.Red);
            }
            hra.spriteBatch.TextSeStinem(hra.fontCourierNew, "Strana " + (strana + 1), new Vector2(930, 550), Color.Red);
            break;
        // vykreslení chyby načtení
        case eStav.ChybaNacteni:
            hra.spriteBatch.TextSeStinem(hra.fontCourierNew, $"{klient.ChybaNacitani} \n Zkusit znovu? [A/N]", new Vector2(510, 240), Color.Red);
            break;
        // vykreslení chyby uložení
        case eStav.ChybaUlozeni:
            hra.spriteBatch.TextSeStinem(hra.fontCourierNew, $"{klient.ChybaUkladani} \n Zkusit znovu? [A/N]", new Vector2(510, 240), Color.Red);
            break;

    }
    hra.spriteBatch.End();

    base.Draw(gameTime);
}

Nejkomplikovanější je vykreslení stavu výpisu rekordů. Ostatní stavy jen vypisují nějaký text. Na jaké jsme straně udává atribut strana, výchozí hodnota je 0. Vypočítáme index prvního a posledního rekordu na této straně, tedy jaké instance Hrac z výsledků hráčů chceme vypsat. Na jednu stranu vypíšeme vždy 12 záznamů. Poté projedeme všechna čísla mezi prvním a posledním a z pole vypíšeme hráče a jejich výsledky na obrazovku. Nakonec nezapomeneme vypsat na jaké stránce se nacházíme. Je to sice maličkost, ale v takovýchto maličkostech je spokojenost uživatele (hráče) :).

Update()

V metodě Update() to bude daleko zajímavější. Zde budeme hlídat klávesnici a odchytávat uživatelský vstup, přesněji klávesy A a N a dokonce pak všechna písmena pro zápis přezdívky hráče. Ukažme si nejprve kompletní kód metody:

public override void Update(GameTime gameTime)
{
    // návrat o do menu
    if (hra.klavesy.IsKeyDown(Keys.Escape) == true)
        hra.PrepniObrazovku(hra.obrazovkaMenu);

    // výběr chování dle aktuálního stavu
    switch (stav)
    {
        // dotaz při chybě načtení
        case eStav.ChybaNacteni:
            if (hra.NovaKlavesa(Keys.A)) // ano
                Nacti();
            if (hra.NovaKlavesa(Keys.N)) // ne
                hra.PrepniObrazovku(hra.obrazovkaMenu);
            break;
        // dotaz při chybě uložení
        case eStav.ChybaUlozeni:
            if (hra.NovaKlavesa(Keys.A))
                Uloz();
            if (hra.NovaKlavesa(Keys.N))
                hra.PrepniObrazovku(hra.obrazovkaMenu);
            break;
        // dotaz na nahrání skóre na internet
        case eStav.Otazka:
            if (hra.NovaKlavesa(Keys.A))
                stav = eStav.Zapis;
            if (hra.NovaKlavesa(Keys.N))
                hra.PrepniObrazovku(hra.obrazovkaMenu);
            break;
        // zadávání přezdívky
        case eStav.Zapis:
            // získání všech stisknutých kláves
            Keys[] stisknuteKlavesy = hra.klavesy.GetPressedKeys();
            foreach (var klavesa in stisknuteKlavesy)
            {
                // klávesa je nově stisknuta
                if (hra.klavesyMinule.IsKeyUp(klavesa))
                {
                    // mazání backspacem
                    if ((klavesa == Keys.Back) && (prezdivka.Length > 0))
                        prezdivka = prezdivka.Remove(prezdivka.Length - 1, 1);
                    else
                    // mezerník
                    if (klavesa == Keys.Space)
                        prezdivka = prezdivka.Insert(prezdivka.Length, " ");
                    else
                    // potvrzení enterem
                    if (klavesa == Keys.Enter)
                    {
                        hra.hrac.prezdivka = prezdivka;
                        Uloz();
                        // vynulujeme uložené body
                        hra.hrac.body = 0;
                    }
                    else
                    // ostatní klávesy - vložení písmen
                    {
                        string pismeno = klavesa.ToString();
                        if (pismeno.Length == 2)
                            pismeno = pismeno.Replace("D", "");
                        pismeno = pismeno.Replace("NumPad", "");
                        // přidání písmene do přezdívky
                        if ((pismeno.Length == 1) && (prezdivka.Length < 9))
                            prezdivka += pismeno;
                    }
                }
            }
            break;
        // přepínání stran ve výpisu tabulky
        case eStav.Vypis:
            if (hra.klavesy.IsKeyDown(Keys.Up) && hra.klavesyMinule.IsKeyUp(Keys.Up))
                if (strana > 0)
                    strana--;
            if (hra.klavesy.IsKeyDown(Keys.Down) && hra.klavesyMinule.IsKeyUp(Keys.Down))
                if (strana < (int)Math.Ceiling((double)hraci.Count / 12) - 1)
                    strana++;
            break;
        }

        base.Update(gameTime);
}

Nejsložitější je v tomto případě zadání přezdívky hráče, zbylé stavy jsou jen triviální reakce na klávesy A a N.

U zadání přezdívky dáme hráči možnost zapsat jakoukoli přezdívku bude chtít. Očekávání je možnost psát jakýkoli znak a možnost napsaný znak smazat. Omezíme pouze délku přezdívky. Budeme číst stisknuté klávesy a převádět je přímo na řetězec. Ale má to háček, čísla na NumPad mají prefix "NumPad" a čísla v horní části klávesnice mají prefix "D". Odstranit "NumPad" je snadné, použijeme metodu Replace() přímo na řetězci a u písmena "D" zkontrolujeme délku zaznamenaného znaku, protože chceme odstranit jen "D" před znakem a nikoli skutečné písmeno "D" :)

OnEnabledChanged()

Ještě vás seznámím s hezkou přetížitelnou metodou OnEnabledChanged(), která přijde velice vhod, pokud chceme přepnout komponentu do aktivního stavu a současně jí uvést do výchozího stavu. Například budeme chtít, aby se hra nevypla, pokud prohrajeme. Kdybychom se vrátili do menu přepnutím obrazovky a pak zpět do hry, tak zjistíme, že hra je ve stavu v jakém jsme jí skončili a není možné jí hrát. A právě proto přijde na scénu metoda OnEnabledChanged(). Upravíme ještě komponentu KomponentaLevel na znovu spuštění po prohře. Úpravy budou vypadat takto:

using System; // přidat using pro EventArgs, pokud není

protected override void OnEnabledChanged(object sender, EventArgs args)
{
    if (Enabled)
    {
        stavHry = eStavHry.Hra;
        hra.PrepniHudbu(hra.hudba_zardax);
        hra.hrac = new Hrac();
        hraciPlocha.Vyprazdni();
        rychlost = 1;
        // vygenerování a dosazení kostek
        pristiKostka = generatorKostek.Generuj(7);
        DalsiKostka();
    }
    base.OnEnabledChanged(sender, args);
}

A v Update() při vytvoření nové kostky upravíme takto

if (hraciPlocha.Kolize(kostka, kostka.pozice))
    hra.PrepniObrazovku(hra.obrazovkaMenu);

Po těchto úpravách budete moci opakovaně hrát bez nutnosti hru znovu zapínat. Šikovné že? :)

Příště, v lekci Hra tetris v MonoGame: Dokončení, dokončíme klienta na ukládání výsledků a tím naší sérii uzavřeme.


 

Stáhnout

Staženo 5x (13.23 MB)
Aplikace je včetně zdrojových kódů v jazyce C#

 

 

Aktivity (3)

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!