Lekce 6 - Bojovník do arény - Zapouzdření
V předešlém cvičení, Řešené úlohy k 5. lekci OOP v C++, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
Z předchozích lekcí máme svůj první pořádný objekt, byla jím hrací kostka. Tento a příští C++ tutoriál o objektově orientovaném programování budou věnovány zprovoznění naší arény. Hrací kostku již máme, ještě nám chybí další objekt: bojovník. Nejprve si popišme, co má bojovník umět, poté se pustíme do psaní kódu.
Na úvod si prosím vymažte výpisy do konzole v konstruktorech a destruktoru ve třídě Kostka. Pro další práci by se nám tyto výpisy pletly do výstupu.
Atributy
Bojovník bude mít určité životy (zdraví). 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 40 životů z 80-ti. Bojovník má určitý útok a obranu. Když bojovník útočí se sílou 20 na druhého bojovníka s obranou 10, ubere mu 10 životů (výpočet později zdokonalíme). Bojovník bude mít referenci na instanci objektu Kostka. Při útoku či obraně si vždy hodí kostkou a k útoku/obraně se 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 25". Zprávami se zatím nebudeme zatěžovat a vrátíme se k nim až nakonec.
Již víme, co budeme dělat, pojďme na to! K projektu arény si přidáme
třídu Bojovnik a dodejme jí patřičné atributy.
Bojovnik.h
#ifndef __BOJOVNIK_H_ #define __BOJOVNIK_H_ #include <string> #include "Kostka.h" using namespace std; class Bojovnik { public: float zivot; float max_zivot; float utok; float obrana; Kostka &kostka; }; #endif
Konstruktor a destruktor jsme prozatím smazali. Stejně tak nesmíme zapomenou naincludovat Kostka.h k Bojovníkovi.
Metody
Pojďme pro atributy vytvořit konstruktor, nebude to nic těžkého. Budeme chtít nastavit všechny atributy, které třída má. Přidáme deklaraci do hlavičkového souboru Bojovnik.h a inicializaci do implementačního:
Bojovnik.cpp
Bojovnik::Bojovnik(float zivot, float utok, float obrana, Kostka &kostka) : kostka(kostka) { this->zivot = zivot; this->max_zivot = zivot; this->utok = utok; this->obrana = obrana; }
Všimněte si, že maximální 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 život a maximální život bude stejný. Dále si všimněte inicializace reference. Reference je použita z toho důvodu, abychom v programu měli pouze jednu kostku. Pokud by se nejednalo o referenci (nebo ukazatel), potom by měl každý bojovník vlastní kostku. Jak jsme si řekli v lekci o referencích, reference musí být inicializovaná hned při vytvoření. A jak víme z předchozích lekcí, to je ještě před tím, než se zavolá konstruktor. Proto musíme použít tuto syntaxi. Obecně by všechny atributy u kterých to jde, měly být inicializovány tímto způsobem. Proto si konstruktor ještě dodatečně přepíšeme:
Bojovnik::Bojovnik(float zivot, float utok, float obrana, Kostka &kostka) : kostka(kostka), zivot(zivot), max_zivot(zivot), utok(utok), obrana(obrana) {}
Nyní je to správně podle dobrých praktik.
Přejděme k metodám. Zřejmě budeme potřebovat nějakou metodu nazivu(), která zjistí, jestli bojovník ještě žije. Stejně tak budeme potřebovat metodu utoc(), pomocí které bojovník zaútočí na jiného bojovníka. Nejprve se podíváme na metodu nazivu(). Vyjdeme z toho, jestli má bojovník ještě nějaké životy. Pokud nemá, potom je zřejmě mrtvý.
bool Bojovnik::nazivu() { if (this->zivot > 0) return true; else return false; }
Protože výraz "this->zivot > 0
" vrací logickou hodnotu,
můžeme celou metodu přepsat do následující podoby:
bool Bojovnik::nazivu() { return this->zivot > 0; }
S podobnou a zbytečnou podmínkou se setkávám často i u pokročilejších programátorů, proto na ni schválně upozorňuji. If-else konstrukce v takovém případě vypovídá pouze o nezkušenosti programátora.
Nyní se podíváme na metodu utoc(). Ta na základě obrany, hodu kostky a útoku vypočítá zranění, tak jak jsme si to popsali na začátku.
void Bojovnik::utoc(Bojovnik & druhy) { float obrana_druhy = druhy.obrana + druhy.kostka.hod(); float utok_prvni = this->utok + this->kostka.hod(); float zraneni = utok_prvni - obrana_druhy; if (zraneni < 0) zraneni = 0; druhy.zivot -= zraneni; }
Výpočet by měl být snadný. Vypočítáme si obranu obránce (se započteným hodem kostky), poté útočnou sílu útočníka (opět se započteným hodem kostky) a tyto hodnoty od sebe odečteme - získáváme poškození. Musíme počítat i se situací, kdy je obrana vyšší než útok - proto ona podmínka (pokud by tam nebyla, poté by se obránci doplňovaly životy). Nakonec od životů obránce odečteme poškození a tím jsme skončili.
Viditelnost
Nyní máme bojovníky, kteří mezi sebou mohou bojovat. Co když ale jeden
z hráčů chce podvádět a bude chtít protihráči odebrat více životů?
Pokud by to byl programátor, který našeho bojovníka používá (například
protože je v knihovně), tak může, protože jsme mu dovolili volně měnit
počet životů. Jedním ze základních pilířů OOP je tzv.
zapouzdření, tedy uchovávat si atributy pro sebe a ven
vystavovat jen metody. To zařizuje ona magická část public:
na
začátku třídy.
Upravme si třídu bojovníka následovně:
Bojovnik.h
class Bojovnik { private: float zivot; float max_zivot; float utok; float obrana; Kostka &kostka; public: Bojovnik(float zivot, float utok, float obrana, Kostka &kostka); bool nazivu(); void utoc(Bojovnik &druhy); };
Všimněte si použití private:
na začátku třídy. Všechny
atributy (a metody) následující za touto konstrukcí nebudou viditelné z
vnějšku. Po této úpravě například nemůžeme provést tento kód:
Bojovnik ja(100, 8, 4, &kostka); Bojovnik protovnik(100, 8, 4, &kostka); ja.zivoty = 99999; // Haha, jsem téměř nesmrtelný ja.obrana = 99999; // Haha, jsem nezranitelný protivnik.utok = 0; // Haha, neublížíš ani mouše protivník.zivoty = 1; // Haha, jsi slaboch
Programátor nebude mít přístup ani k jednomu z atributů, protože jsou privátní. Možná by se ale někomu hodilo vědět, kolik má bojovník ještě životů. Na to se používají tzv. gettery a settery. Jedná se metody které začínají get nebo set následující názvem atributu. V principu to jsou plnohodnotné metody, které vrací nebo nastavují hodnotu pro privátní atribut. Gettery a settery z nich dělá pouze konvence.
Například pro životy by vypadaly metody následovně:
Bojovnik.h
class Bojovnik { private: float zivot; float max_zivot; float utok; float obrana; Kostka &kostka; public: Bojovnik(float zivot, float utok, float obrana, Kostka &kostka); bool nazivu(); void utoc(Bojovnik &druhy); float getZivot(); // getter void setZivot(float zivot); // setter };
Bojovnik.cpp
float Bojovnik::getZivot() { return this->zivot; } void Bojovnik::setZivot(float zivot) { if (zivot < 0) // validace return; this->zivot = zivot; }
Pomocí metody getZivot()
si nyní můžeme zjistit život
bojovníka a to i když je atribut privátní. Naopak pomocí setteru můžeme
život nastavit. Všimněte si, že není možné nastavit život na hodnotu
nižší než 0 kvůli podmínce v settery. Setter pro životy mít v našem
případě nechceme, protože zranění se řeší v metodě utoc(),
ale chtěl jsem jej ukázat také z toho důvodu, že můžeme dodat validaci
vstupu. Pokud by byl atribut veřejně přístupný, pak tuto validaci nemáme
jak zařídit a bojovníkovi by opravdu někdo mohl nastavit záporné
zdraví.
K atributům a metodám, které jsou označeny jako public, tedy můžeme přistupovat z vnějšku. Naopak atributy a metody označené jako private jsou zabezpečené a máme jistotu, že jediný, kdo je vidí, jsme my. K těmto metodám a atributům můžeme přistupovat z jiných metod ve stejné třídě (i z veřejných) a přes ukazatel this. To je princip zapouzdření - chránit si svá vlastní data před změnou.
Souboj
Nyní můžeme provést takový malý souboj. V main.cpp si vytvoříme dva bojovníky a budou mezi sebou bojovat až do doby, kdy jeden z nich nezemře (nezapomeneme includovat Bojovnik.h).
#include <iostream> #include "Kostka.h" #include "Bojovnik.h" using namespace std; int main() { Kostka kostka; Bojovnik prvni(100, 8, 4, kostka); Bojovnik druhy(100, 8, 4, kostka); while (prvni.nazivu() && druhy.nazivu()) { prvni.utoc(druhy); if (druhy.nazivu()) druhy.utoc(prvni); } if (prvni.nazivu()) cout << "Prvni bojovnik vyhral s " << prvni.getZivot() << " zivoty" << endl; else cout << "Druhy bojovnik vyhral s " << druhy.getZivot() << " zivoty" << endl; cin.get(); return 0; }
#ifndef __KOSTKA_H__ #define __KOSTKA_H__ using namespace std; class Kostka { private: int pocet_sten; public: Kostka(); Kostka(int pocet_sten); int hod(); int getPocetSten(); }; #endif
#include <iostream> #include <cstdlib> #include <ctime> #include "Kostka.h" using namespace std; Kostka::Kostka() : Kostka(6) { } Kostka::Kostka(int pocet_sten) { this->pocet_sten = pocet_sten; srand((unsigned int)time(NULL)); } int Kostka::hod() { return rand() % this->pocet_sten + 1; } int Kostka::getPocetSten() { return this->pocet_sten; }
#ifndef __BOJOVNIK_H_ #define __BOJOVNIK_H_ #include <string> #include "Kostka.h" using namespace std; class Bojovnik { private: float zivot; float max_zivot; float utok; float obrana; Kostka &kostka; public: Bojovnik(float zivot, float utok, float obrana, Kostka &kostka); bool nazivu(); void utoc(Bojovnik &druhy); float getZivot(); }; #endif
#include "Bojovnik.h" Bojovnik::Bojovnik(float zivot, float utok, float obrana, Kostka &kostka) : kostka(kostka), zivot(zivot), max_zivot(zivot), utok(utok), obrana(obrana) {} bool Bojovnik::nazivu() { return this->zivot > 0; } void Bojovnik::utoc(Bojovnik & druhy) { float obrana_druhy = druhy.obrana + druhy.kostka.hod(); float utok_prvni = this->utok + this->kostka.hod(); float zraneni = utok_prvni - obrana_druhy; if (zraneni < 0) zraneni = 0; druhy.zivot -= zraneni; } float Bojovnik::getZivot() { return this->zivot; }
Konvence
Určitě jste si všimli, že metody a atributy jsou psány jiným stylem
(například velikost písma, mezery apod.). V C++ (na rozdíl třeba od Javy
nebo C#), nejsou dány pravidla, jak se má kód psát. Vývojáři se dokonce
ani neshodli, jak psát složené závorky - jestli za název metody (jak to
dělá například Java) nebo pod metodu (jak to dělá C#). Osobně dodržuji
konvenci uvedenou v seriálu, tedy názvy atributů a proměnných jsou v tzv.
snake-case notaci: malým písmenem a oddělené podtržítkem
(int nejaka_promenna
, string nazev_hrace
). Metody
píšu v tzv. camelCase notaci (nazevNejakeMetody()
,
hod()
). Konstanty potom v ALL_CAPS notaci
(MAX_POCET_HRACU
, POCET_LEVELU
). Co se závorek
týče, používám tzv. Allmanův styl (složené závorky pod názvem metody),
různé konvence můžete najít na Wikipedii. V tomto není
C++ sjednocené a je na každém programátorovi, aby si vybral svůj styl.
To by bylo pro tuto lekci vše. V projektu jsme upravili viditelnosti i pro ostatní třídy a popřípadě doplnili gettery a settery. Pokud chcete mít jistotu, že pracujeme nad stejným kódem, stáhněte si prosím zdrojové kódy dole pod článkem.
V příští lekci s ním budeme pokračovat.
A co nás příště čeká? V lekci Aréna s bojovníky v C++ si napíšeme nějakou základní funkcionalitu arény, aby již program něco dělal.
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 99x (527.01 kB)
Aplikace je včetně zdrojových kódů v jazyce C++