Akce! Dobij si body, napiš nám do zpráv "Přes léto se to naučím!" a dobijeme ti ještě navíc 50% z této částky! Sleva na výuku platí do 22.6.2018.

Lekce 5 - Bojovník do arény v Dartu

Dart Objektově-orientované programování Bojovník do arény v Dartu

ONEbit hosting 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é lekci, Odkazy na objekty, jejich kopírování a garbage collector, jsme si vysvětlili jak fungují odkazy na objekty, garbage collector a hluboká kopie. Již tedy víme, jak fungují reference a jak můžeme s objekty zacházet. Bude se nám to hodit dnes i příště. Tento a příští Dart tutoriál budou totiž věnovány dokončení naší arény. Hrací kostku již máme, ještě nám chybí další 2 objekty: bojovník a samotná aréna. Dnes se budeme věnovat bojovníkovi. Nejprve si popišme, co má bojovník umět, poté se pustíme do psaní kódu.

Vlastnosti

Bojovník se bude nějak jmenovat a bude mít určitý počet hp (tedy života, např. 80hp). Budeme uchovávat jeho maximální život (bude se lišit u každé instance) a jeho současný život, tedy např. zraněný bojovník bude mít 40hp z 80ti. Bojovník má určitý útok a obranu, obojí vyjádřené opět v hp. Když bojovník útočí s útokem 20hp na druhého bojovníka s obranou 10hp, ubere mu 10hp života. Bojovník bude mít referenci na instanci objektu Kostka. Při útoku či obraně si vždy hodí kostkou a k útoku/obraně přičte padlé číslo. (Samozřejmě by mohl mít každý bojovník svou kostku, ale chtěl jsem se přiblížit stolní podobě hry a ukázat, jak OOP opravdu simuluje realitu. Bojovníci tedy budou sdílet jednu instanci kostky.) Kostkou dodáme hře prvek náhody, v realitě se jedná vlastně o štěstí, jak se útok nebo obrana vydaří. Konečně budeme chtít, aby bojovníci podávali zprávy o tom, co se děje, protože jinak by z toho uživatel nic neměl. Zpráva bude vypadat např. "Zalgoren útočí s úderem za 25hp.". Zprávami se zatím nebudeme zatěžovat a vrátíme se k nim až nakonec.

class Bojovnik {
        /// Jméno bojovníka
        String _jmeno;

        /// Život v HP
        int _zivot;

        /// Maximální zdraví
        int _maxZivot;

        /// Útok v HP
        int _utok;

        /// Obrana v HP
        int _obrana;

        /// Instance hrací kostky
        Kostka _kostka;
}

Třída Kostka musí samozřejmě být v našem projektu.

Metody

Pojďme pro vlastnosti vytvořit konstruktor, nebude to nic těžkého. Komentáře zde vynechám, vy si je dopište podobně, jako u vlastností výše. Nebudu je psát ani u dalších metod, aby se tutoriál zbytečně neroztahoval a zůstal přehledný.

Bojovnik(this._jmeno, this._maxZivot, this._utok, this._obrana, this._kostka) {
        _zivot = _maxZivot;
}

Všimněte si, jak je tato zkracující syntaxe pro parametry v konstruktoru praktická. :-)

Zdraví si v konstruktoru odvodíme a nemáme na něj parametr v hlavičce metody. Předpokládáme, že bojovník je při vytvoření plně zdravý, stačí nám tedy znát pouze jeho maximální život a život bude stejný. Zbytek vlastností se inicializuje sám díky klíčovému slovu this.

Přejděme k metodám. Opět se nejprve zamysleme nad tím, co by měl bojovník umět. Začněme tím jednodušším, budeme chtít nějakou textovou reprezentaci, abychom mohli bojovníka vypsat. Překryjeme tedy metodu toString(), která vrátí jméno bojovníka. Určitě se nám bude hodit metoda, vracející zda je bojovník naživu (tedy typu bool). Aby to bylo trochu zajímavější, budeme chtít kreslit život bojovníka do konzole, nebudeme tedy psát, kolik má života, ale "vykreslíme" ho takto:

[#########    ]

Výše uvedený život by odpovídal asi 70%. Dosud zmíněné metody nepotřebovaly žádné parametry. Samotný útok a obranu nechme na později a pojďme si implementovat toString(), nazivu() a grafickyZivot(). Začněme s toString(), tam není co vymýšlet:

@override
String toString()
{
        return _jmeno;
}

Nyní implementujme metodu nazivu(). Opět to nebude nic těžkého. Stačí zkontrolovat, zda je život větší než 0 a podle toho se zachovat. Mohli bychom ji napsat třeba takto:

bool nazivu()
{
        if (_zivot > 0)
                return true;
        else
        return false;
}

Jelikož i samotný výraz (zivot > 0) je vlastně logická hodnota, můžeme vrátit tu a kód se značně zjednoduší:

bool nazivu()
{
        return _zivot > 0;
}

Grafický život

Jak jsem se již zmínil, metoda grafickyZivot() bude umožňovat vykreslit ukazatel života v grafické podobě. Již víme, že z hlediska objektového návrhu není vhodné, aby metoda objektu přímo vypisovala do konzole (pokud není k výpisu objekt určený), proto si znaky uložíme do řetězce a ten vrátíme pro pozdější vypsání. Ukážeme si kód metody a následně podrobně popíšeme:

String grafickyZivot() {
        String s = '[';
        int celkem = 20;
        int pocet = ((_zivot / _maxZivot) * celkem).round();
        if (pocet == 0 && nazivu())
                pocet = 1;
        for (int i = 0; i < pocet; i++)
                s += '#';
        s = s.padRight(celkem + 1);
        s += ']';
        return s;
}

Připravíme si řetězec s a vložíme do něj úvodní znak "[". Určíme si celkovou délku ukazatele života do proměnné celkem (např. 20). Nyní v podstatě nepotřebujeme nic jiného, než trojčlenku. Pokud _maxZivot odpovídá celkem dílků, _zivot bude odpovídat pocet dílkům. pocet je proměnná s počtem dílků aktuálního zdraví.

Matematicky platí, že pocet = (zivot / maxZivot) * celkem;. My ještě doplníme zaokrouhlení na celé dílky. Měli bychom ošetřit případ, kdy je život tak nízký, že nám vyjde na 0 dílků, ale bojovník je stále naživu. V tom případě vykreslíme 1 dílek, jinak by to vypadalo, že je již mrtvý.

Dále stačí jednoduše for cyklem připojit k řetězci s patřičný počet znaků a doplnit je mezerami do celkové délky. Doplnění provedeme přidáním mezery za řetězec, kterou několikrát vynásobíme operátorem *, čímž se doplní prázdná část života. Přidáme koncový znak a řetězec vrátíme.

Celý kód můžeme ještě velmi zjednodušit; sami popřemýšlejte jak kód funguje:

String grafickyZivot() {
        int celkem = 20;
        int pocet = ((_zivot / _maxZivot) * celkem).round();
        if (pocet == 0 && nazivu())
                pocet = 1;
        return '[' + '#' * pocet + ' ' * (celkem - pocet) + ']';
}

Vše si vyzkoušíme, přejděme do main.dart a vytvořme si bojovníka (a kostku, protože tu musíme konstruktoru bojovníka předat). Následně vypišme, zda je naživu a jeho život graficky:

Kostka kostka = new Kostka(10);
Bojovnik bojovnik = new Bojovnik('Zalgoren', 100, 20, 10, kostka);

print('Bojovník: $bojovnik'); // test toString();
print('Naživu: ${bojovnik.nazivu()}'); // test nazivu();
print('Život: ${bojovnik.grafickyZivot()}'); // test grafickyZivot()

Výstup programu:

Konzolová aplikace
Bojovník: Zalgoren
Naživu: true
Život: [####################]

Boj

Dostáváme se k samotnému boji. Implementujeme metody pro útok a obranu.

Obrana

Začněme obranou. Metoda branSe() bude umožňovat bránit se úderu, jehož síla bude předána metodě jako parametr. Metodu si opět ukážeme a poté popíšeme:

void branSe(int uder) {
        int zraneni = uder - (_obrana + _kostka.hod());
        if (zraneni > 0) {
                _zivot -= zraneni;
                if (_zivot <= 0) {
                        _zivot = 0;
                }
        }
}

Nejprve spočítáme skutečné zranění a to tak, že z útoku nepřítele odečteme naši obranu zvýšenou o číslo, které padlo na hrací kostce. Pokud jsme zranění celé neodrazili (zraneni > 0), budeme snižovat náš život. Tato podmínka je důležitá, kdybychom zranění odrazili a bylo např. -2, bez podmínky by se život zvýšil. Po snížení života zkontrolujeme, zda není v záporné hodnotě a případně ho dorovnáme na nulu.

Útok

Metoda utoc() bude brát jako parametr instanci bojovníka, na kterého se útočí. To proto, abychom na něm mohli zavolat metodu branSe(), která na náš útok zareaguje a zmenší protivníkův život. Zde vidíme výhody referencí v Dartu. Můžeme si instance jednoduše předávat a volat na nich metody, aniž by došlo k jejich zkopírování. Jako první vypočteme úder, podobně jako při obraně, úder bude náš útok + hodnota z hrací kostky. Na soupeři následně zavoláme metodu branSe() s hodnotou úderu:

void utoc(Bojovnik souper) {
        int uder = _utok + _kostka.hod();
        souper.branSe(uder);
}

To bychom měli, pojďme si zkusit v našem ukázkovém programu zaútočit a poté znovu vykreslit život. Pro jednoduchost nemusíme zakládat dalšího bojovníka, ale můžeme zaútočit sami na sebe:

Kostka kostka = new Kostka(10);
Bojovnik bojovnik = new Bojovnik('Zalgoren', 100, 20, 10, kostka);

print('Bojovník: $bojovnik'); // test toString();
print('Naživu: ${bojovnik.nazivu()}'); // test nazivu();
print('Život: ${bojovnik.grafickyZivot()}'); // test grafickyZivot()

bojovnik.utoc(bojovnik); // test útoku
print('Život po útoku: ${bojovnik.grafickyZivot()}');

Výstup programu:

Konzolová aplikace
Bojovník: Zalgoren
Naživu: True
Život: [####################]
Život po útoku: [##################  ]

Zdá se, že vše funguje, jak má. Přejděme k poslednímu bodu tohoto tutoriálu a to ke zprávám.

Zprávy

Jak již bylo řečeno, o útocích a obraně budeme uživatele informovat výpisem na konzoli. Výpis nebude provádět samotná třída Bojovnik, ta bude jen vracet zprávy jako textové řetězce. Jedna možnost by byla nastavit návratový typ metod utoc() a branSe() na String a při jejich volání vrátit i zprávu. Problém by však nastal v případě, když bychom chtěli získat zprávu od metody, která již něco vrací. Metoda samozřejmě nemůže jednoduše vrátit 2 věci.

Pojďme na věc univerzálněji, zprávu budeme ukládat do privátní proměnné zprava a uděláme metody pro její uložení a navrácení. Samozřejmě bychom mohli udělat proměnnou veřejnou, ale není zde důvod, proč umožnit zvenčí zápis do zprávy a také by skládání složitější zprávy uvnitř třídy mohlo být někdy problematické.

K vlastnostem třídy tedy přidáme:

String _zprava;

Nyní si vytvoříme dvě metody. Privátní nastavZpravu(), která bere jako parametr text zprávy a slouží pro vnitřní účely třídy, kde nastaví zprávu do privátní proměnné:

void _nastavZpravu(String zprava)
{
        _zprava = zprava;
}

Nic složitého. Podobně jednoduchá bude veřejná metoda pro navrácení zprávy:

String vratPosledniZ­pravu() {
return _zprava; }

O práci se zprávami obohatíme naše metody utoc() a branSe(), nyní budou vypadat takto:

void utoc(Bojovnik souper) {
        int uder = _utok + _kostka.hod();
        _nastavZpravu('$_jmeno útočí s úderem za $uder hp');
        souper.branSe(uder);
}

void branSe(int uder) {
        String zprava = '';
        int zraneni = uder - (_obrana + _kostka.hod());
        if (zraneni > 0) {
                _zivot -= zraneni;
                zprava = '$_jmeno utrpěl poškození $zraneni hp';
                if (_zivot <= 0) {
                        _zivot = 0;
                        zprava += ' a zemřel';
                }
        } else {
                zprava = '$_jmeno odrazil útok';
        }
                _nastavZpravu(zprava);
        }

Vše si opět vyzkoušíme, tentokrát již vytvoříme druhého bojovníka:

Kostka kostka = new Kostka(10);
Bojovnik bojovnik = new Bojovnik('Zalgoren', 100, 20, 10, kostka);

print('Život: ${bojovnik.grafickyZivot()}'); // test GrafickyZivot();

// útok na našeho bojovníka
Bojovnik souper = new Bojovnik('Shadow', 60, 18, 15, kostka);
souper.utoc(bojovnik);
print(souper.vratPosledniZpravu());
print(bojovnik.vratPosledniZpravu());

print('Život: ${bojovnik.grafickyZivot()}');

Výstup programu:

Konzolová aplikace
Život: [####################]
Shadow útočí s úderem za 27 hp
Zalgoren utrpěl poškození 13 hp
Život: [#################   ]

Máme kostku i bojovníka, teď již chybí jen aréna. Tu si vytvoříme hned v příští lekci, Aréna s bojovníky v Dartu.


 

Stáhnout

Staženo 0x (2.38 kB)
Aplikace je včetně zdrojových kódů v jazyce Dart

 

 

Článek pro vás napsal Honza Bittner
Avatar
Jak se ti líbí článek?
1 hlasů
Milovník Dartu. Student FIT ČVUT. Sleduj mě na https://twitter.com/tenhobi a ptej se na cokoli na https://github.com/tenhobi/ama.
Aktivity (2)

 

 

Komentáře

Avatar
dez1nd
Člen
Avatar
dez1nd:5. ledna 12:19

nechci být hnidopich, ale utok a obrana se neoznacuje jako HP :)
HP jako health points
AP jako attack points
DP jako defense points..
jestli máš pro HP i jiné vysvětlení, prosím sem sním, rád se přiučím

 
Odpovědět 5. ledna 12:19
Avatar
Honza Bittner
Šupák
Avatar
Odpovídá na dez1nd
Honza Bittner:5. ledna 12:27

HP se zde (stejně jako v obdobných OOP kurzech na této síti) používá jako jednotka útoku a obrany pro jednoduchost. Prostě ti to značí hodnotu životů, které může bojovník snížit tomu druhému atp. :-)

Samozřejmě by se mohla zavést speciální jednotka pro útok i obranu a pak to magicky dopočítávat stejně na tyto hodnoty, které to má teď, ale to není smyslem lekcí. :-)

Odpovědět 5. ledna 12:27
Milovník Dartu. Student FIT ČVUT. Sleduj mě na https://twitter.com/tenhobi a ptej se na cokoli na https://github.com/...
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.

Zobrazeno 2 zpráv z 2.