Avatar
Tommy
Člen
Avatar
Tommy:

Ahoj, vytvářím velmi jednoduchý herní framework a rád bych se vás zkušenějších zeptal na pár věcí, které se týkají OOP v C++. Jsou to spíše takové jemnosti, které nemají přímý vliv na funkčnost toho, co dělám, ale v budoucnu by se mohli projevit jako špatný návrh. Zkrátka chci mít pevný základ pro programování jednoduchých 2D her a jelikož se jedná o školní projekt, tak nemám moře času na učení se metodou pokus-omyl. Některé otázky jsou možná trochu obecné (jiné zase debilní), tak mě zajímá váš pohled na věc. Akceptuji skutečnost, že někdy neexistuje nejlepší univerzální řešení.

1. Singleton
Používám ho tam, kde mám jistotu, že nebudu potřebovat další instanci a chci globální přístup. K instanci přistupuji pomocí pointeru (dynamická alokace), a tak mě zajímá, jak je to s pamětí? Singleton "žije" od začátku do konce programu (resp. hry) a při ukončení programu by měl správně OS uvolnit i s ním spojenou paměť. Na to bych se ale asi neměl spolehát. Mám se tedy nějak postarat o uvolnění paměti? Použít místo pointeru referenci nebo smart pointer, případně máte jiný nápad či postřeh k singletonům?

2. Abstraktní základní třída
Mám dva adepty: herní stavy (menu, pauza, game over, ...) a objekty (to, co hráč ve hře vidí). Jde o to, že využívám polymorfismus, a tak, aby vše fungovalo jak má, je nutné vnutit těmto adeptům určité rozhraní (např. všechny objekty se musí vykreslit). Má ale vůbec smysl v tomto případě použít AZT?

Např. z AZT objekt stejně první zdědím svou vlastní třídu objekt, která bude mít navíc např. parametr pro souřadnice, kde se má vykreslit. A až z ní má smysl zdědit konkrétní objekty jako jsou tlačítka nebo postavy. Ovšem, když chci něco vykreslit, tak stejně musím vždy vědět kde (vždy stejný parametr pro souřadnice). A já nevím jestli je lepší mít takto 2 oddělené třídy objekt nebo je spojit v jednu, která už nejspíš nebude AZT, neboť filosofií AZT je definovat pouze rozhraní.

3. Konstruktory
Z tohoto důvodu (vykreslování, viz předešlý dotaz) nemá ani moc smysl používat u herních objektů implicitní konstruktor. Je správné tedy implicitní konstruktor nastavit jako privátní? A pokud tam ponechám tu AZT, pak parametrický konstruktor potomka musí volat i ten samý konstruktor předka a jaký by měl mít parametrický konstruktor smysl u AZT? (vidíte, jak jsem se v tom zamotal?)

4. Multiplatformnost
Tohle je poprvé, co používám multiplatformní technologie (C++, SDL 2.0). Pokud něco vytvořím na Windows a budu chtít, aby to jelo na Linuxu, případně na Macu, pak to musím na dané platformě i zkompilovat? (vycházím z toho, že C++ je kompilovaný jazyk a nemám tušení, jak vypadají spustitelné soubory na Linuxu a Macu)

Tak to je zatím vše. Budu rád za každou vaši radu :-). Chci to mít prostě vyladěný, nespokojím se s tím, že to "jen" funguje, akorát zatím nemám to potřebné know-how, co se týče OOP.

 
Odpovědět 29.10.2015 17:54
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Tommy
patrik.valkovic:

1 - OS skutečně uvolní všechnu paměť, kterou program používal. Z toho plyne, že smaže i Singleton. Eventuelně si můžeš udělat nějakou statickou metodu, která ti singleton smaže, ale není to moc podle návrhu singletonu.
2 - AZT nemusí nutně definovat pouze rozhraní. V C++ je možné (na rzdíl od ostatních programovacích jazyků) definovat tělo i abstraktním metodám. Vždy je lepší programovat proti "rozhraní", i když se bude jednat o abstraktní třídu. Souřadnice můžeš určitě vymyslet jinak. Já jsem to řešil tak, že jsem ze třídy, která objekty vykreslovala, vkládal souřadnice vykreslení jako parametr. Je to nejefektivnější řešení.
3 - Pokud nadefinuješ nějaký kontruktor (s parametrama), tak implicitní se sám nedefinuje, tudíž jej nemusíš skrývat.
4 - Pro Linux můžeš zkompilovat programy i pro Linux (Cygwin + g++). Ale nemůžeš použít kompilátor pro Windows. Také si musíš dát pozor, abys nepoužíval knihovny specifické pro platformu ("windows.h").

Nahoru Odpovědět 29.10.2015 18:22
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
Odpovídá na Tommy
Lukáš Hruda (Luckin):

1. Nepíšeš tu přesně na co ten singleton potřebuješ, ale obecně platí, že používat singleton nebývá dobrý nápad.
Pokud ho použiješ, co se týká úklidu daného objektu, záleží jakým způsobem ho vytvoříš. Pokud objekt přímo deklaruješ jako atribut, např. takto

class Singleton
{
        static Singleton singleton;
        .
        .
        .
};

pak se s koncem programu zavolá destruktor singletonu a ten bude korektně zničen.
Pokud ale objekt vytvoříš dynamicky takto:

class Singleton
{
        Singleton* singleton;
        .
        .
        .
};
Singleton::singleton = new Singleton;

pak ti objekt v programu zůstane viset a úklid provede až operační systém, na což sice u moderních OS lze spoléhat, ale obecně je to prasárna.

2. Udělat abstraktní třídu jako rozhraní pro vykreslovatelné obejekty je určitě dobrý nápad. Ideálně aby neměla žádné atributy ani implemetaci, tedy sloužila skutečně pouze jako rozhraní. Nevadí, když bude mít jenom jednu metodu.
Nikdy nevíš, kdy budeš potřebovat objekt, který žádnou pozici mít nebude a přesto půjde vykreslit, například pozadí.
Snaž se ale nepřehánět to s tou dědičností (obrovký strom dědičnosti, kde každý další objekt přidává jednu funkci nebo kde se člověk musí prokousat několika abstraktními úrovněmi než narazí a třídu, která jde instancovat, je spíš matoucí), obzvlášť s tou vícenásobnou a když, tak si o ní nejdřív něco nastuduj.

3. Osobně bych implicitní konstruktor vytvořil. Jeho absence totiž může způsobit problémy. Pokud budeš chtít například vytvořit pole objektů nebo vkládat objekty do kolekce (např. vektor), nepůjde to. Samozřejmě to lze vždy udělat tak, že použiješ pole/kolekci pointerů a budeš pak objekty vytvářet dynamicky, což bývá i často rozumnější, ale někdy se víc vyplatí ukládat tam přímo objekty a ne jen ukazatele, nemusíš se pak například starat o jejich dealokaci a neprovádíš tolik alokací na haldě, které mohou někdy být relativně pomalé.
Možná to vůbec nevyužiješ, ale jelikož píšeš, že děláš framework, což je něco, co by mělo být poměrně obecné, nepřijde mi jako dobrý nápad tohle prostě zakázat, obzvlášť, když vytvoření implicitního konstruktoru je jeden řádek. Stejně tak by každá třída ideálně měla mít korektně definovaný kopírovací konstruktor a úplně ideálně i operátor =. Nikdy nevíš, co se s tou třídou bude jednou dělat.

 
Nahoru Odpovědět 29.10.2015 19:30
Avatar
Tommy
Člen
Avatar
Tommy:

Díky Lukáš Hruda (Luckin) a patrik.valkovic za vaše odpovědi, pokusím se tedy ještě upřesnit pár věcí

 
Nahoru Odpovědět 29.10.2015 22:01
Avatar
Tommy
Člen
Avatar
Tommy:

1. Z toho, co jsem vygooglil, se singleton většinou definuje tak, jak píše Luckin (ta 2. varianta), proto i já jej zatím definoval takto. Ten memory leak je tam naprosto zjevný, ale nikde na něj nijak neupozorňují (což mě znepokojuje). Jelikož se většinou singleton (nebo aspoň v mém případě) používá vždy od začátku do konce programu, tak potřebuji uvolnit onu paměť opravdu až ke konci. Tedy těsně předtím, než to stejně udělá OS. Nevím tedy, jestli je zodpovědností programátora rešit, že uživatel má nějaký velmi starý nebo buglý OS (kolik takových uživatelů asi je?). Jenže už zase slyším ty nepříjemné dotazy od učitelů. Je to jen teorie... Bude tedy lepší držet se spíše té 1. varianty?

Jinak, já používám jako singleton např. třídu, která obstarává práci s texturami. Tzn. obsahuje pole textur a metody pro jejich přidávání a vykreslování. Je potřeba jen jedna v průběhu celé hry (od jejího začátku až do konce) a metody jsou volány na různých místech, takže ten globální přístup je nutný. (pozn.: to vykreslování je nejdřív pouze na backbuffer, výsledné překreslení na frontbuffer se provede v herní smyčce, o kterou se stará třída "hra")

Třídu pro hru jsem zatím udělal jako singleton taky, což je možná kontroverzní. Je to hlavně proto, že obsahuje front i back buffer, ale backbuffer potřebuje i ta třída pro textury (když je tam vykresluje). Nikdy jsem nepoužíval globální proměnné a nevím jak je to lepší. Teď mě ale napadlo, že dám ten backbuffer do třídy pro textury, takže třída hra už nemusí být singleton ...

4. Já používám MinGW a pouze standardní knihovny C++ a multiplatformní SDL. Moc jsem nepochopil, jak ten Cygwin má fungovat. Jde mi o to, že když to teda zkompiluju na Windows s pomocí MinGW, tak *.exe nepojede klasicky třeba na Macu. Pokud ovšem vezmu zdrojáky a rebuildnu to pomocí nějakého kompilátoru pro Mac na Macu, tak by se to mělo zkompilovat a měl bych tak získat vlastně spustitelnou verzi pro Mac. To bych potom považoval za multiplatformnost. No a jestli to tak skutečně, to zatím nevím.

2., 3. Pokusím se tu zítra přidat kousek nějakého kódu pro lepší ilustraci. Jelikož je to jen školní projekt (k maturitě), tak to stejně kromě mě dál nikdo nevyužije, proto to nechci moc přehánět s tou obecností (nikdo to neocení a akorát si zkomplikuji život).

Jinak ještě díky patrik.valkovic k 3. bodu, to jsem netušil.

 
Nahoru Odpovědět 29.10.2015 22:03
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Tommy
patrik.valkovic:

Memory leak ti tam nemůže vzniknout už z principu OS. Pokud nepracuješ s real-time systémy, musí se starat OS o správu paměti. Tedy nemůže stejnou pamět přiřadit několika procesům. Když program skončí, tak se automaticky (ve všech systémech) paměť uvolní. Kdyby se paměť neuvolnila po ukončení programu, tak ti bude na RAM ležet kus paměti, kterou OS nemůže použít ani pro jiné procesy, což je samozřejmě blbost.
V real-time OS to funguje na trošku jiném principu, ale základ je v tom, že ti běží pouze jediný proces, který má všechny prostředky. Jakmile se proces ukončí, tak příjde další proces, který má zase stejné prostředky. Co se může maximálně stát je, že další proces bude mít někde v paměti uložené data předchozího procesu, ale pokud neřešíš věci jako hesla, šifrování a podobné věci, kde ti teoreticky můžou zůstat "citlivé" informace uložené v RAM, tak tě to vůbec nemusí trápit.
Prostě řekni, že OS si automaticky paměť uvolní, a nemusíš se o to tedy jako programátor starat.

Cygwin ti prakticky přenese příkazovou řádku Linuxu do Windows. Máš tam bash a podobné věci, které můžeš používat. Můžeš si tam stáhnout g++ kompiler, který funguje prakticky stejně jako MSBuild a podobně. Co vím, tak MinGW funguje na stejném principu.

V g++ pomocí přepínače nastavuješ možnosti

g++ Source.cpp -mcall-linux # měl by se zkompilovat pro Linux

pomocí přepínače -b nastavíš architekturu a tak. Viz http://linux.die.net/man/1/g++
Ale nemyslím si, že by tě to mělo zajímat, když se jedná jen o školní projekt ;-)

Nahoru Odpovědět 29.10.2015 22:29
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
Odpovídá na Tommy
Lukáš Hruda (Luckin):

Memory leak ti na beznem modernim OS jako windows skutecne nevznikne, alespon ne na urovni OS, vznikne ti jen na urovni programu, ale u singletonu to bude az v dobe, kdy uz je ti to jedno.
Problem ale bude, pokud by ten objekt pracoval s necim mimo ten program, treba v konstruktoru oteviral soubor, socket nebo pripojeni k databazi. To uz za tebe OS nezavre (ten soubor jeste nejspis ano, ale ne hned). Tyhle uklidove akce budes idealne delat v destruktoru, ale v pripade te dynamicke alokace se destruktor sam od sebe nezavola. Proto je dobre si nezvykat to takhle delat. Proste je to prasarna - ke kazdemu new musi patrit jeden delete.

 
Nahoru Odpovědět 30.10.2015 0:08
Avatar
Tommy
Člen
Avatar
Tommy:

Lukáš Hruda (Luckin), patrik.valkovic

Opět děkuji za vaše odpovědi. Otázka multiplatformnosti opravdu není nijak horká, jen by byla škoda ji nevyužít (když můžu). Singletony rovněž považuji za vyřešené. Teď se pokusím nastínit v čem mám problém s tou AZT.

Mám klasickou herní smyčku, která 1. načte vstup, 2. provede update, 3. render - všechno vykreslí na obrazovku.

Hra se může současně nacházet v dané chvíli v jednom "stavu" (menu, game over, ...). Abych tedy nemusel použít u updatu a renderování nějaký switch, chci implementovat FSM. Ta by měla pointer na jakousi obecnou třídu "stav" a při volání update a render by se tedy využil polymorfismus. Třída stav by pak obsahavola pole pointerů na jakousi obecnou třídu "objekt" (pro menu tlačítka, pro samotnou hru postavy, ...), tudíž update a render se provede opět s pomocí polymorfismu.

No a já si nejsem jistý, zda-li bych měl použít pro ony obecné třídy "objekt" a "stav" AZT. Taky tápu nad tím, jak moc obecně bych to měl navrhnout? Např.:

class azt_objekt {
public:
        virtual void update() = 0;
        virtual void render() = 0;
        virtual ~objekt() {}
};

Pak konkrétní třída by mohla vypadat třeba takto:

class had : public azt_objekt {
private:
        vector2d *p_telo;
public:
        virtual void update() {...}
        virtual void render() {...}
};

Mám pak v plánu naprogramovat hada...

Analogicky bych vytvořil třeba třídu pro "potravu". Je to opravdu poprvé, co se snažím programovat objektově (znovupoužitelný kód), kdybych to dělal procedurálně, tak by to až takový problém nebyl. Holt se stejně někdy to OOP budu muset naučit. (možná by to chtělo ty objekty navrhnout ještě nějak jinak :-D)

 
Nahoru Odpovědět 30.10.2015 13:01
Avatar
Tommy
Člen
Avatar
Tommy:

Ahoj, jelikož už nejspíš nikdo nemá zájem přispět k tomuto tématu :-(... Označím jej tedy za vyřešené. Na AZT jsem nakonec nějak vyzrál, udělal řadu dalších změn a začínám to vidět docela nadějně.

Naposledy ještě jednou díky za všechny vaše postřehy a rady

Akceptované řešení
+5 Zkušeností
Řešení problému
 
Nahoru Odpovědět 4.11.2015 3:27
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 9 zpráv z 9.