Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 15 - Hra tetris v MonoGame: Skóre

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.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

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

 

Předchozí článek
MonoGame: Skrolující text (autoři hry)
Všechny články v sekci
Od nuly k tetrisu v MonoGame
Přeskočit článek
(nedoporučujeme)
Hra tetris v MonoGame: Dokončení
Článek pro vás napsal Matouš Kratochvíl
Avatar
Uživatelské hodnocení:
2 hlasů
Autor se věnuje C#
Aktivity