IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

15. diel - Hra tetris v MonoGame: Skóre

V minulom dieli seriálu MonoGame tutoriálov, MonoGame: Skrolující text (autori hry) , sme si vytvorili posúvajúce text s autormi. Dnes hru obohatíme o tabuľku skóre, ktorej sa, ako máme sľúbené, internetová. Tak smelo do toho.

Komponenta skóre tabuľky

Tabuľka skóre bude komponent. A ako mnohí tušia, tak do zložky Komponenty/ vytvoríme triedu s názvom KomponentaSkoreTabulka a podědíme ju z DrawableGameComponent. Ďalej pridáme privátne premenné pre Hra a Texture2D s názvom pozadi. V LoadContent() textúru načítame. Doteraz všetko známe:

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();
    }
}

Zmyslom tejto komponenty bude ukladanie a zobrazovanie dosiahnutých výsledkov v našej hre. Po skončení hry kolízií novovytvorenej kocky sa hra prepne na obrazovku skóre, kde zobrazí uložené výsledky a vyzve aktuálneho hráča na uloženie výsledku pod prezývku. K tomu budeme potrebovať ešte pár premenných: reťazec pre prezývku a kolekciu uložených výsledkov (rekordov). K tomu máme triedu Hrac. A pretože výsledkov môže byť na viac stránok, tak si budeme ešte držať aktuálne stánku. Tieto premenné do triedy KomponentaSkoreTabulka pridáme:

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

Skóre klient

Výsledky budeme ukladať na server a následne sťahovať do súboru. Preto vytvoríme špeciálnu triedu SkoreKlient, ktorá bude zabezpečovať ukladanie a načítanie dát skóre tabuľky. V komponente budeme len volať jej metódy, menovite Nacti() a Uloz(), prípadne si tu získame text chybových hlášok. Túto triedu si zatiaľ len pripravme, doprogramujeme ju nabudúce:

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()
        {
        }
}

Rovno sme si pripravili aj názov súboru, do ktorého nabudúce skóre z internetu stiahneme.

Vrátime sa späť do triedy KomponentaSkoreTabulka a inštanciu SkoreKlient pridáme medzi jej privátne atribúty:

private SkoreKlient klient;

A v neposlednom rade nezabudneme, všetko čo sme doteraz deklarovali, inicializovať:

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

Stav skóre tabuľky

Odchytávanie chýb pri načítaní alebo ukladaní skóre celkom ošemetná záležitosť zvlášť potom v aplikáciách využívajúce update-render slučku. My to vyriešime fikan pomocou enum stavov tabuľky a konštrukcie switch.

Najprv si zadefinujeme enum eStav a vytvorené enum použijeme ako typ pre premenou stav:

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

private eStav stav;

Skóre tabuľka môže teraz buď ukazovať chybu načítanie, ukazovať chybu uloženie, zapisovať nový rekord alebo vypisovať tabuľku rekordov. A čo hodnota Otazka ? Tú použijeme v prípade, že sa aplikácia používateľa pýta, či si želá uložiť svoje dosiahnuté skóre.

Uloženie a načítanie skóre tabuľky

Stav budú meniť najmä metódy Uloz() a Nacti(). Využijeme v nich try - catch blok pri volaní metód klienta, ktorého sme si pripravili vyššie. Stav zmeníme ak všetko prejde alebo aj keď bola vyhodená výnimka.

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 ()

Teraz ešte napísať metódy Update() a Draw(). Začneme metódou 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);
}

Najkomplikovanejšie je vykreslenie stavu výpisu rekordov. Ostatné stavy len vypisujú nejaký text. Na aké sme strane udáva atribút strana, predvolená hodnota je 0. Vypočítame index prvého a posledného rekordu na tejto strane, teda aké inštancie Hrac z výsledkov hráčov chceme vypísať. Na jednu stranu vypíšeme vždy 12 záznamov. Potom prejdeme všetky čísla medzi prvým a posledným az poľa vypíšeme hráčov a ich výsledky na obrazovku. Nakoniec nezabudneme vypísať na akej stránke sa nachádzame. Je to síce maličkosť, ale v takýchto maličkostiach je spokojnosť používateľa (hráča) :) .

Update ()

V metóde Update() to bude ďaleko zaujímavejšie. Tu budeme strážiť klávesnicu a odchytávať užívateľský vstup, presnejšie klávesy A a N a dokonca potom všetky písmená pre zápis prezývky hráčov. Ukážme si najprv kompletnú kód metódy:

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);
}

Najzložitejšia je v tomto prípade zadania prezývky hráčov, zvyšné stavy sú len triviálne reakcie na klávesy A a N.

U zadanie prezývky dáme hráči možnosť zapísať akúkoľvek prezývku bude chcieť. Očakávania je možnosť písať akýkoľvek znak a možnosť napísaný znak zmazať. Obmedzíme iba dĺžku prezývky. Budeme čítať stlačené klávesy a prevádzať ich priamo na reťazec. Ale má to háčik, čísla na NumPad majú prefix "NumPad" a čísla v hornej časti klávesnice majú prefix "D". Odstrániť "NumPad" je ľahké, použijeme metódu Replace() priamo na reťazci au písmená "D" skontrolujeme dĺžku zaznamenaného znaku, pretože chceme odstrániť len "D" pred znakom a nie skutočné písmeno "D" :)

OnEnabledChanged ()

Ešte vás zoznámim s peknou preťažiteľný metódou OnEnabledChanged(), ktorá príde veľmi vhod, ak chceme prepnúť komponent do aktívneho stavu a súčasne jej uviesť do východiskového stavu. Napríklad budeme chcieť, aby sa hra nevypla, ak prehráme. Keby sme sa vrátili do menu prepnutím obrazovky a potom späť do hry, tak zistíme, že hra je v stave v akom sme jej skončili a nie je možné ju hrať. A práve preto príde na scénu metóda OnEnabledChanged(). Upravíme ešte komponent KomponentaLevel na znovu spustenie po prehre. Úpravy budú vyzerať 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() pri vytvorení novej kocky upravíme takto

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

Po týchto úpravách budete môcť opakovane hrať bez nutnosti hru znova zapínať. Šikovné že? :)

Nabudúce, v lekcii Hra tetris v MonoGame: Dokončenie , dokončíme klienta na ukladanie výsledkov a tým našej série uzavrieme.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 17x (13.23 MB)
Aplikácia je vrátane zdrojových kódov v jazyku C#

 

Predchádzajúci článok
MonoGame: Skrolující text (autori hry)
Všetky články v sekcii
Od nuly k tetrisu v MonoGame
Preskočiť článok
(neodporúčame)
Hra tetris v MonoGame: Dokončenie
Článok pre vás napísal Matouš Kratochvíl
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje C#
Aktivity