Lekce 6 - Hra tetris v MonoGame: Kostka
V minulé lekci, Rozdělení MonoGame hry do komponent, jsme si ukázali rozdělení hry do komponent.
Dnes konečně začneme s Tetrisem, přesněji s padající kostkou.
Instance kostky budou ve hře vždy jen 2. Jedna aktuálně padající, druhá ta, co padne příště. Protože jich tedy bude více a jedná se o objekt ve hře, nepoužijeme k reprezentaci kostky komponentu, ale pouze jednoduchou třídu. Komponenta obvykle spravuje větší celek logiky. Po dopadu se kostka spojí s hrací plochou, která tedy nebude kolekcí kostek, ale jednoduše dvourozměrným polem políček. Určitě by se hra dala řešit i jinak, ale tento způsob mi přišel nejjednodušší.
Vytvoříme si třídu reprezentující kostku Tetrisu. K projektu
Robotris
přidáme novou třídu Kostka
, její
modifikátor přístupu nastavíme na public
.
public class Kostka
Kostce přidáme veřejnou vlastnost Policka
, která bude
dvourozměrný polem typu int
. Tam budou uložena jednotlivá
políčka, ze kterých se kostka skládá. Pole má rozměr 4x4 (kvůli té
nejdelší kostce tvaru I), mohli bychom si ho s kostkou tvaru S představit asi
takto:
0110 1100 0000 0000
1
označuje plné políčko, 0
prázdné. Setter
vlastnosti označíme jako privátní, ale není to nutné a podobného
výsledku bychom dosáhli i obyčejným atributem. Ve hrách se na vlastnosti
nedbá tolik, jak jsme zvyklí, je to mimojiné i proto, že jsou pomalejší a
u hry většinou požadujeme maximální výkon.
public int[,] Policka { get; private set; }
Dále přidáme pozici kostky na hrací ploše, bude typu
Vector2
a bude se jednat o běžnou, veřejnou proměnnou. Jelikož
je Vector2
struktura (tedy hodnotový typ), měli bychom při
použití vlastností problém jednoduše modifikovat její složky. Určitě se
nebojte používat úplně obyčejné veřejné proměnné, ve hře se na to
tolik nehraje. Přídáme potřebný using
:
using Microsoft.Xna.Framework;
a atribut:
public Vector2 pozice;
Jelikož víme, že pole jsou v C# řešeny referencí, nemůžeme jednoduše
dosadit políčka jedné kostky do políček druhé. Výsledkem by bylo, že by
obě kostky používaly ta samá políčka a když by se jedna třeba orotovala,
orotovaly by se obě. Protože políčka budeme kopírovat často, vytvoříme
si k tomu metodu. Ta políčka znovu založí a dosadí do nich hodnoty z těch,
která kopírujeme. Okopírované pole metoda vrátí. Celého efektu dosáhneme
jednoduše pomocí 2 for
cyklů.
private int[,] ZkopirujPolicka(int[,] policka) { int [,] nova = new int [4, 4]; for (int j = 0; j < 4; j++) for (int i = 0; i < 4; i++) nova[i, j] = policka[i, j]; return nova; }
Implementujme nyní konstruktor třídy, ten bude brát v parametru políčka, ze kterých kostku vytvoří. Dále zde resetujeme pozici kostky:
public Kostka(int[,] policka) { Policka = ZkopirujPolicka(policka); pozice = new Vector2(0, 0); }
Pád
Přidejme si jednoduchou metodu Spadni()
, která vyvolá pád
kostičky o 1 úroveň:
public void Spadni() { pozice.Y++; }
Rotace
To bylo takové odhlehčující Teď zas něco těžšího, napíšeme si metodu, která naše pole
orotuje. Rotovat budeme ve směru hodinových ručiček. Rotace dosáhneme tak,
že pole okopírujeme a z této kopie budeme políčka dosazovat do políček
původních. Trik je v tom, že prohodíme X a Y souřadnice políček a 2.
souřadnici odečteme od pozice 3. Pro lepší pochopení přikládám
ilustraci:

Pokud stále tápete, zkuste si to namalovat a papír otáčet. Pokud ani to
nepomohlo, budete mi prostě muset věřit Metoda vypadá takto:
public void Orotuj() { // pomocné pole int[,] a = ZkopirujPolicka(Policka); // rotace pole jako matice prohozením souřadnic for (int y = 0; y < 4; y++) for (int x = 0; x < 4; x++) Policka[x, y] = a[y, 3 - x]; }
Rotace má problém, který v tutoriálu z důvodu jednoduchosti opomeneme. Některé kostky jsou rozměru 2x2 (v základní verzi jen kostka O), některé 3x3 a některé (v základní verzi jen kostka I) rozměru 4x4. Metoda výše rotuje kostku vždy jako pole 4x4 a menší kostky tedy nejsou centrovány. Toho by se dalo docílit nějakou další kontrolou a pokud budete naléhat, mohu ji po dokončení kurzu doplnit.
Zbývá kostku nějak vykreslit. Již jsme si ukázali, jak se vykreslují
komponenty. U kostky to bude podobné, také ji přidáme metodu
Vykresli()
. SpriteBatch
a příslušnou texturu
políčka do ní dostaneme přes parametr. Je to kromě komponent, kde se
předávala instance hry v konstruktoru, druhý způsob, jak se vypořádat se
závislostmi. Opět opakuji, že tento způsob je lepší u jednotlivých
herních objektů (např. jednotlivé kostky), komponenty jsou spíše pro celky
(např. level obsahující kostky, hrací plochu...). Jelikož souřadnice
kostky odpovídají souřadnicím v hrací ploše (tedy např. [6; 10]) a ne
souřadnicím na obrazovce, přidáme ještě parametr okraj, který nám
umožní kostku posouvat. Metoda projede jednotlivá políčka a ty s hodnotou
větší než 0
vykreslí. V cyklech budeme pracovat s rozměry
jednoho políčka, abychom je mohli skládat vedle sebe, rozměr zjistíme
vlastnostmi Width
a Height
na instanci textury.
public void Vykresli(Vector2 okraj, LepsiSpriteBatch spriteBatch, Texture2D sprite) { for (int j = 0; j < 4; j++) for (int i = 0; i < 4; i++) if (Policka[i, j] > 0) spriteBatch.Draw(sprite, new Vector2(okraj.X + (i + pozice.X) * sprite.Width, okraj.Y + (j + pozice.Y) * sprite.Height), Color.White); }
Kvůli použití Texture2D
přidáme using
:
using Microsoft.Xna.Framework.Graphics;
Třídu prozatím necháme a kostku si vyzkoušíme. Stáhněte si sprity
políček z archivu pod článkem. Jedná se o sadu 15ti spritů pro políčka.
Složku Policka/
rozbalte a celou ji přetáhněte do složky
Sprity/
v MonoGame Pipeline Tool. Výsledek by měl vypadat
takto:

Přesuneme se do komponenty KomponentaLevel
. Zde přidáme 3
privátní atributy:
private Kostka kostka; private Vector2 poziceHraciPlochy; private Texture2D spritePolicka;
První je instance právě padající kostky, druhý je pozice hrací plochy na pozadí levelu, třetí je sprite pro políčko.
V Initialize()
do kostky dosadíme novou instanci třídy
Kostka
. Jelikož ještě nemáme generátor kostek a kostka
potřebuje v konstruktoru vzor, ze kterého se má vytvořit, vytvoříme si na
zkoušku jedno pole sami. Jelikož se v .NETu můžeme spolehnout, že nové
celočíselné pole má v prvcích samé nuly, stačí nám nastavit 4 hodnoty,
třeba té kostky, jak je výše u obrázku k otáčení:
int[,] vzor = new int[4, 4]; vzor[1, 0] = 1; vzor[2, 0] = 1; vzor[0, 1] = 1; vzor[1, 1] = 1; kostka = new Kostka(vzor);
Ve stejné metodě ještě nastavíme proměnnou
poziceHraciPlochy
:
poziceHraciPlochy = new Vector2(366, 50);
V LoadContent()
načteme sprite nějakého políčka do námi
připravené proměnné:
spritePolicka = hra.Content.Load<Texture2D>(@"Sprity\Policka\5");
Nyní se přesuneme do Draw()
a za vykreslení pozadí přidáme
vykreslení kostky:
kostka.Vykresli(poziceHraciPlochy, hra.spriteBatch, spritePolicka);
Zde vidíte, jak kostka kód čistí, stará se o své vykreslování sama, i když ho musíme volat. Tak by to mělo být u všech herních objektů.
Vyzkoušíme:

Přidáme obsluhu rotace. Do Update()
doplníme reakci na
Enter nebo šipku nahoru:
if (hra.NovaKlavesa(Keys.Enter) || hra.NovaKlavesa(Keys.Up))
kostka.Orotuj();
Opět můžete vyzkoušet. Až na nešvar vzniklý rotací kostky 3x3 jako pole 4x4 to funguje docela dobře.
Příště, v lekci Hra tetris v MonoGame: Generátor kostek, si naprogramujeme generátor vzorů kostek a naučíme kostku náhodně generovat sprity políček.
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 435x (11.9 MB)
Aplikace je včetně zdrojových kódů v jazyce C#