Úvodem
Po dokončení minulého článku jsem hodně přemýšlel, jakým směrem se v tomto seriálu ubírat dál. Jedna možnost by byla věnovat se postupně jednotlivým částem knihovny odděleně na malých příkladech. Takový přístup je poměrně typický a často velmi účelný. Přesto jsem se v tomto případě nakonec rozhodl, že se pokusím postupovat jinak. Malé příklady mají totiž tu nevýhodu, že jsou velice umělé. ClanLib je objektová knihovna a C++ je objektově oriantovaný jazyk a jejich síla se projevuje obvykle až u rozsáhlejších projektů. Jelikož bych rád vám čtenářům ukázal sílu ClanLibu, rozhodl jsem se společně s vámi vytvořit jednoduchou hru inspirovanou hrami jako je Dynablaster či AtomicBomberman. Doufám, že to tak bude i větší zábava.
Pokud se však časem ukáže, že nás tento přístup odvádí od ClanLibu samotného (což by nebyla chyba ani ClanLibu ani OOP, ale moje), vrátíme se ke krátkým příkladům.
Začněme tedy se slibovanými soubory pro definici zdrojů (resource definition files, dále jen RDF ). Řekněme, že se naše hra bude jmenovat „Onion Bomberman“. Ještě nevíme, zda bude nakonci běžet v okně, nebo ve fullscreenu, ale během vývoje bude pohodlnější, když poběží v okně. Nemůžeme si však nyní být ani jistí, jaká bude velikost tohoto okna a i název hry v titulku by se mohl změnit.
Přesto něco víme :-). Víme, že budeme potřebovat okno a nástroj jak měnit výše uvedené parametry co nejpohodlněji. A přesně k tomuto účelu využijeme RDF.
Vytvoříme tedy soubor zdroje.xml s následujícím obsahem v adresáři zdroje:
<resources> <section name="HlavniOkno"> <string name="Titulek" value="Onion Bomberman"/> <integer name="SirkaOkna" value="640"/> <integer name="VyskaOkna" value="480"/> <boolean name="FullScreen" value="false"/> </section> </resources>
To je formát přímo podporovaný knihovnou. Jednotlivé položky jsou umístěny mezi tagy <resources> </resources> a mohou (nemusí) být navíc organizovány do pojmenovaných sekcí. Podporovanými typy jsou string (CL_String), integer (CL_Integer), float (CL_Float), boolean (CL_Boolean) a mnoho dalších poněkud specifičtějších typů, ke kterým se dostaneme v příštích dílech, např. Sprite (CL_Sprite).
Jak ale takto zapsaný string nebo integer získáme v našem programu? Jednoduše tak, že nejprve vytvoříme správce těchto našich zdrojů (resource manager):
CL_ResourceManager SpravceZdroju("zdroje/zdroje.xml");
Zde uvádíme těžko konfigurovatelnou cestu, a proto je třeba mít ji dobře rozmyšlenou.
Nyní už je získání údajů ze souboru zdroje.xml velice jednoduché, jak ukazuje příklad:
int SirkaOkna = CL_Integer("HlavniOkno/SirkaOkna", &SpravceZdroju);
Podíváme-li se nyní na program ahojsvete z minulého dílu, zjistíme, že ho můžeme lehce předělat na základ naší nové hry. Třídu T_AhojSveteApp přejmenujeme na třídu T_Aplikace. V hlavičkovém souboru to bude vlastně vše. Ve zdrojovém souboru ještě uděláme několik úprav.
Je velice pravděpodobné, že Správce zdrojů budeme potřebovat na spoustě různých míst, a proto ukazatel na něj umístíme do globálního jmnenného prostoru Konfigurace. Vytvořit ho však můžeme teprve po inicialicaci knihovny (před deinicializací ho zrušíme):
...
CL_SetupGL::init();
// vytvorime SpravceZdroju definovanych v zdroje/zdroje.xml
SpravceZdroju = new CL_ResourceManager("zdroje/zdroje.xml");
V metodě main() řádky
// vytvorime okno 640 x 480 pixelu s titulkem Ahoj, Svete!
const string Titulek = "Ahoj, Svete!";
const int SirkaOkna = 640;
const int VyskaOkna = 480;
const bool FullScreen = false;
nahradíme následujícím kódem:
// vytvorime okno s parametry uvedenými ve zdroje/zdroje.xml
const string Titulek = CL_String::load("HlavniOkno/Titulek", SpravceZdroju);
const int SirkaOkna = CL_Integer("SirkaOkna", SpravceZdroju);
const int VyskaOkna = CL_Integer("VyskaOkna", SpravceZdroju);
const bool FullScreen = CL_Boolean("FullScreen", SpravceZdroju);
Abychom se ke třídě T_Aplikace již nemuseli vracet a aby řešila jen to nejnutnější, dohodneme se, že vlastní chod naší hry obstará metoda Run() nové třídy T_Hra. Do metody Run() prozatím přesuneme vytvoření Loga a while smyčku.
Na jejich místo dodáme do metody main() následující kód:
// spustime hru T_Hra Hra; Hra.Run();
Aby jsme se neupsali při includování hlavičkových souborů ClanLibu, přesuneme příslušné direktivy do souboru hlavicky.h.
Zamysleme se nyní, co bychom mohli chtít po třídě T_Hra. Je docela pravděpodobné, že metoda Run() bude obsahovat smyčku potobnou té, kterou v ní už máme.
Hry se obvykle skládají z několika poměrně nezávislých částí. V našem případě by to přinejmenším mohlo být jakési úvodní menu, na kterém si budeme moci předvést tvorbu GUI, a vlastní hra dvou hráčů proti sobě, říkejme jí bitva. Při programování bitvy se zase hlouběji seznámíme s partiemi, jako jsou sprite v ClanLibu a detekce kolizí.
Opět je však možné, že se v budoucnu rozhodneme dodat ještě další módy, jako třeba úvodní animaci využívající OpenGL a podobně.
Tyto módy budou mít i něco společného. Při každém kroku metody Run() naší hry bude aktuální aktivní mód nějakým způsobem aktualizovat svou logiku a následně se vykreslí.
To by nás do budoucna mohlo přivést k myšlence odvozovat všechny módy od základní třídy T_ModHryZaklad s virtuálními metodami AktualizujLogiku() a VykresliSe(), volanými třídou T_Hra v každém kroku pro aktuální mód.
Každopádně se nám tu opět rýsuje docela pěkný příklad na využití RDF. Tentokrát však budeme potřebovat trochu silnější nástroje, než ty, s nimiž jsme se setkali doposud.
Řekněme, že bychom chtěli z RDF získat seznam všech módů, které chceme v konkrétním okamžiku zahrnovat do programu, a tento seznam bychom opět chtěli mít možnost pohodlně modifikovat.
Takový seznam by mohl být v souboru zdroje.xml v sekci „T_Hra“. Zde by bylo vhodné poznamenat také jméno módu, který má T_Hra nastavit jako aktivní při své konstrukci. Mohlo by to vypadat třeba takto:
<section name="T_Hra"> <string name="UvodniMod" value="Menu"/> <ModHry name="Menu"/> <ModHry name="Bitva"/> </section>
Získat jméno úvodního módu už umíme. Podívejme se tedy na to, jak získat seznam všech módu. Přesně řečeno, chceme seznam všech XXX ve výrazech <ModHry name=„XXX“/>. Řešení může vypadat třeba takto:
//-----------------------------------------------
// NactiSeznamModu()
//-----------------------------------------------
void T_Hra::NactiSeznamModu() {
using namespace std;
using namespace Konfigurace;
// nejprve smazeme aktualni seznam
SeznamModu.clear();
// vytvorime seznam vsech jmen polozek typu ModHry
// v sekci T_Hra ve zdroje.xml
typedef list<string> TTmpList;
TTmpList TmpList = SpravceZdroju->get_resources_of_type("ModHry", "T_Hra");
// vytvorime seznam jmen pripustnuch modu
typedef TTmpList::iterator TTmpListIt;
TTmpListIt eIt = TmpList.end();
for (TTmpListIt It = TmpList.begin(); It != eIt; ++It) {
// vytvorime mod prislusneho nazvu
CL_Resource ModHry = SpravceZdroju->get_resource(*It);
// nahrajeme jeho data
ModHry.load();
// nyni jiz muzeme pristupovat k jeho slozkam pres CL_DomElement
CL_DomElement Element = ModHry.get_element();
// zjistime tedy jeho jmeno
string JmenoModu = Element.get_attribute("name");
// toto jmeno pridame do vytvareneho seznamu
SeznamModu.push_back(JmenoModu);
}
} // NactiSeznamModu() --------------------------
SeznamModu je nějaký kontejner stringů, například vector nebo list. Jeho vyprázdnění před načítáním asi nikoho nepřekvapí. Mnohem zajímavější je řádek
TTmpList TmpList = SpravceZdroju->get_resources_of_type("ModHry", "T_Hra");
Metoda get_resources_of_type() vrací list<string>. Jedná se vlastně o seznam cest ke všem položkám typu „ModHry“ v sekci „T_Hra“ vzhledem ke stromové struktuře celého RDF, tj. v našem případě souboru zdroje.xml. Zde bude list obsahovat dva stringy, a to „T_Hra/Bitva“ a „T_Hra/Menu“. Z tohoto příkladu je vidět, že atribut name má poněkud výsadní postavení. Opravdu je tomu tak. Na formát tagů se totiž můžeme dívat následovně:
<Typ name="jmeno" atribut1="retezec1" ... atributN-1="retezecN-1"/>
Máme zde N atributů, z nichž ten nultý name má to výsadní postavení, že je součástí cesty k tagu.
V cyklu přes položky TmpList tj. přes výše popsané cesty nejprve vytvoříme konkrétní reprezentaci tagu pro udanou cestu. Jedná se o instance třídy CL_Resource:
// vytvorime mod prislusneho nazvu
CL_Resource ModHry = SpravceZdroju->get_resource(*It);
// nahrajeme jeho data
ModHry.load();
Děláme to za pomoci metody get_resource(), jejímž parametrem je výše popsaná cesta. Návratovou hodnotou je tedy objekt CL_Resource, do kterého ještě nahrajeme jeho data metodou load().
Přístup k jednotlivým atributům zprostředkovává třída CL_DomElement, kterou vrací metoda get_element() třídy CL_Resource. Konečně se tedy dozvíme to co potřebujeme vědět:
// zjistime tedy jeho jmeno string JmenoModu = Element.get_attribute("name"); // toto jmeno pridame do vytvareneho seznamu SeznamModu.push_back(JmenoModu);
Tento postup může na první pohled vypadat trochu komplikovaně, ale když se nad ním chvíli zamyslíte, a nebo ještě lépe, provedete několik experimentů, zjistíte, že se nejedná o nic hrozného. Tato drobná komplikovanost je asi nezbytnou cenou za univerzálnost práce s RDF.
Je jasné, že pro seznam o dvou položkách se taková práce snad ani nevyplatí. Pokud však máte podezření, že by se mohl seznam v budoucnu rozšiřovat a obětujete těch pár minut navíc, bude to investice, která se vám mnohonásobně vrátí.
Na závěr jsem si nechal ještě malou ukázku využití RDF pro vestavěné ClanLibovské typy. K rozsáhlejším příkladům se dostaneme, až se těmto typům budeme věnovat podrobně. Jedním takovým typem je CL_Surface, kterého se bude ukázka týkat.
Přidejme do zdroje.xml následující sekci:
<section name="Obrazky">
<surface name="Pozadi" file="obrazky/pozadi/pozadi.png"/>
</section>
Když jsme v minulém dílu vytvářeli CL_Sprite, abychom nakreslili Logo knihovny, předávali jsme konstruktoru přímo cestu k souboru s obrázkem. Nyní můžeme postupovat s pomocí RDF mnohem výhodněji.
Vytvoříme obrázek pozadí naší hry. Toto pozadí budeme prozatím vykreslovat v metodě Run() třídy T_Hra. Metoda Run() tedy bude vypadat následovně:
//-----------------------------------------------
// Run()
//-----------------------------------------------
void T_Hra::Run() {
using namespace Konfigurace;
// vytvorime obrazek pozadi !!!!!!!!!!!!!!!!!!!
CL_Surface Pozadi("Obrazky/Pozadi", SpravceZdroju);
// pockame si na stisteni klavesy escape
while (! CL_Keyboard::get_keycode(CL_KEY_ESCAPE)) {
// invariant: Dosud nebyl stisten escape pri get_keycode()
// vykreslime pozadi do leveho horniho rohu !!!!!!!!!!!!!!
Pozadi.draw();
// zobrazime nakreslene zmeny (prehozeni
// predniho a zadniho bufferu)
CL_Display::flip();
// uspime aplikaci na deset milisekund, abychom
// zbytecne neblokovali procesor
const int DobaObnoveni = 10;
CL_System::sleep(DobaObnoveni);
// probudime aplikaci resp. knihovnu aby byla aktualni
CL_System::keep_alive();
} // while
} // Run() --------------------------------------
Stačí už jen upravit rozměry okna na 660×540 a jsme hotovi.
Zdrojové kódy zahrnující i KDevelopí project file dnes vytvořeného programu si můžete stáhnout zde (včetně použitého obrázku).
Podívat se můžete také na všechny dosud napsané zdrojové kódy v pdf (včetně částí, které se do článku nevešly).
V příštím dílu se pokusíme vyřešit dnes nakousnutou otázků módů hry. Podíváme se přitom na sadu chytrých pointerů, které nám ClanLib nabízí. Ty jsou velice užitečné zejména proto, že velmi usnadňují v C++ jinak dosti složitou správu paměti, mají však i další výhody.