Lekce 6 - Bojovník do arény - Zapouzdření

C a C++ C++ Objektově orientované programování Bojovník do arény - Zapouzdření

Unicorn College ONEbit hosting 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, Ukazatel this v C++, jsme si vysvětlili klíčové slovo this. Z předchozích lekcí máme také 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, kteď 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).

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.


 

Stáhnout

Staženo 34x (8.39 kB)
Aplikace je včetně zdrojových kódů v jazyce C++

 

 

Článek pro vás napsal patrik.valkovic
Avatar
Jak se ti líbí článek?
3 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Miniatura
Předchozí článek
Ukazatel this v C++
Miniatura
Následující článek
Aréna s bojovníky v C++
Aktivity (10)

 

 

Komentáře

Avatar
Martin Petrovaj
Překladatel
Avatar
Martin Petrovaj:2. ledna 21:07

Dobrý, v prvom rade musím pochváliť výbornú sériu aj článok, ale okrem toho by som rád dve veci :-)

1 - V časti Metody - A jak víme z "předchozí lekce":{PREVI­OUS_URL}, …

2 - K tej istej časti by som mal aj otázku. Ak som ten zápis ako tomu hovoríte inicializácie parametrov (hlavne referencie na kocku) pochopil správne, tak ten zápis

konstruktor(T1 p1, T2 p2, …) : p1(p1), p2(p2), …

nie sú delegované konštruktory, ale vlastne to je ekvivalent zápisu

this->p1 = p1; this->p2 = p2; …,

ktorý "len" prebehne pred zavolaním tela konštruktoru inicializovaného objektu?

Za odpoveď vopred ďakujem.

Editováno 2. ledna 21:09
Odpovědět 2. ledna 21:07
if (this.motto == "") { throw new NotImplementedException(); }
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Martin Petrovaj
patrik.valkovic:2. ledna 21:23

Ahoj,
díky opraveno.
V principu ano, a je to právě z důvodu referencí. Když bude atribut reference, tak volání this->p1 nepůjde. Reference totiž musí být inicializovaná ještě před spuštěním konstruktoru. To obstarává právě tento zápis (vlastně se jedná o inicializaci proměnných). Podobně pokud by byl atributem třída, která nemá výchozí konstruktor (tedy bezparametrický), muselo by se postupovat tímto způsobem, protože atribut musí být nějak inicializován ještě před vykonáváním konstruktoru.

Odpovědět  +1 2. ledna 21:23
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
Martin Petrovaj
Překladatel
Avatar
Odpovídá na patrik.valkovic
Martin Petrovaj:2. ledna 22:09

Ďakujem za dovysvetlenie. Ešte by som sa ale pre úplné pochopenie chcel spýtať, čo presne sa vlastne deje, keď napr. v našom prípade inicializujeme týmto spôsobom referenciu na objekt typu Kocka. Hlavne mi nejde do hlavy ten zápis - kocka(kocka) (alebo radšej všeobecne atribut(parame­ter)), jednak sa mi zdá zvláštne inicializovať objekt volaním niečoho (predpokladám že nejakého špeciálneho konštruktora? Toť prvá otázka) na kvázi inštancii.

Ak sa takto volá konštruktor, tak to je nejaký automagicky vygenerovaný alebo zvláštny druh? Som z toho trochu mimo preto, lebo mi Visual Studio ponúka (a vlastne aj využívame) overloady s parametrami, ktoré som explicitne nikde nezadefinoval.

Alebo som úplne mimo a zbytočne som sám seba zmiatol a v skutočnosti je to len nejaká osobitná syntax pre poslanie referencie z parametra atribútu inicializovaného objektu? Prepáčte, asi robím z papierovej vlaštovky balistickú strelu, ale snažím sa čo najlepšie a najdetailnejšie pochopiť spôsoby cpp aby som vedel kedy, čo a ako použiť, ale čím ďalej nad tým rozmýšľam, tým viac ma skúsenosti z vyšších jazykov mätú ;-( :-)

No a aby som položil aj jednoduchú otázku dokonca priamo k tomuto článku - cpp asi nemá nič na spôsob vlastností (properties), ako ich poznáme napr. v C#, že? Všetko cez get/setPremenna()

Odpovědět 2. ledna 22:09
if (this.motto == "") { throw new NotImplementedException(); }
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Martin Petrovaj
patrik.valkovic:2. ledna 22:44

Ahoj, C++ properties jako C# nemá (jako getter a setter). Musíš explicitně použít getAtribut a setAtribut.
Co se to toho konstruktoru týče. Konstruktor je prostě metoda, která se zavolá při vytvoření. Problém je, že před tím, než se volá metoda, musíš mít nějakou hodnotu atributů. Představ si tento příklad:

class A{
private:
        int atribut;
public:
        A(){
                cout << this->atribut << endl;
        }
}

Jakou hodnotu program vypíše? Nejspíše nedefinovanou (podle toho co zrovna bylo v paměti). To je ale jen důsledek toho, že typ int nemusí být inicializovaný. Co když ale bude atribut typu třídy.

class A{
public:
        int atribut;
}
class B{
public:
        A instance;
        A(){
                cout << this->instance->atribut << endl;
        }
}

Program pořád půjde spustit, protože se použije výchozí hodnota typu int (nedefinováno) a na třídě A se zavolá bezparametrický konstruktor. Co když ale bude existovat pouze parametrický konstruktor?

class A{
public:
        int atribut;
        A(int val){
                this->atribut = val;
        }
}

Jak bys nyní zavolal tento konstruktor ve třídě B?

class B{
public:
        A instance;
        A(){
                this->instance = new A(5); //není ukazatel
                this->instance = A(5); //volá přiřazovací operátor, ne konstruktor
                this->instance(5); //volá () operátor, ne konstruktor
        }
}

Problém je právě v tom, že instance musí být jednoznačně vytvořena (inicializována) ještě před tím, než se konstruktor zavolá. Potřebujeme tedy zavolat konstruktory atributů. A k tomu je právě ona syntaxe.

class B{
public:
        A instance;
        A() :instance(5) { //vol8n9 konstruktor atributu instance
                cout << this->instance->atribut << endl; //vypise 5
        }
}

Stejný případ je to v případě referencí.
Pokud inicializaci atributu nepoužiješ, použije se by default bezparametrický konstruktor (druhý kód). V případě int to znamená náhodnou hodnotu, nicméně můžeš takto inicializovat i int.

class A{
private:
        int atribut;
public:
        A(): atribut(5) {
                cout << this->atribut << endl; //vypíše 5
        }
}
Odpovědět  +1 2. ledna 22:44
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
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 4 zpráv z 4.