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 17x (13.23 MB)
Aplikace je včetně zdrojových kódů v jazyce C#