----------
Cyp3D
----------

..::|| Zdroje ||::..

:| Myšlenka |:

Každá správná 3D aplikace používá několik základních součástí: textury a meshe. Textury a meshe zabírají nezanedbatenou část grafické paměti, a tak je nemyslitelné, tyto duplikovat s každým objektem, který je používá. Grafické paměti je malé množství a je třeba s ní šetřit, proto název "zdroje". Zde se nabízí otázka, jak tedy provést onu správu zdrojů.

Požadavky:
  1. Je třeba zajistit vazby mezi objekty, které nějaký zdroj potřebují a mezi již existujícími zdroji (zabránění duplikaci)
  2. V případě neužívání zdroje, je vhodné tento uvolnit (nebo alespoň jeho část v grafické paměti)
  3. Zajistit rychlý přístup ke zdrojům (perframe hledání podle identifikátoru je nemyslitelné, kvůli rychlosti).
Moje řešení spočívá v počítání odkazů na zdroj. Všechny zdroje mají společného předka, třídu CCyp3DResource, která se snaží těmto požadavkům vyhovět. Tato třída zavádí funkce na zvýšení a snížení počtu odkazů (IncRef/DecRef), funkce na načtení a uložení infromací o zdroji (Load/Save) a funkce na jeho vytvoření a uvolnění. Princip je následující:

  1. Aplikace dostane povel k načtení skupiny zdrojů (například při načtení mapy). V této chvíly se konstruují instance tříd daných zdrojů a zavolají se jejich členské funkce Load pro načtení infromací o zdroji (př. textura si načte jméno souboru, ze kterého bude stvořena, třeba "OšklivéKačátko.jpg", ale soubor zatím neotevírá) a dojde k "vazbě" mezi zdroji (př: model káčátka najde podle identifikátoru (char*) patřičnou texturu a patřičný mesh). Zatím však nedochází k samotnému vytváření objektů D3D, které zabírají grafickou paměť, ani k zvyšování počtu odkazů.
  2. Aplikace načte skupinu zdrojů a rozhodne se, že je třeba nějaký zdroj použít (viz. dále). Zdroj smí používat jen objekty, které na něj mají platný odkaz (tj. poctivě zvýšili počet odkazů). A aplikace se tedy musí postarat, aby před použitím zdroje byla zavolána IncRef. IncRef zvýší počet odkazů, a pokud jde o první odkaz, zavolá funkci Create, jejímž úkolem je načíst potřebné D3D části do grafické paměti a zvýšit počet odkazů všem zdrojům, které bude dále využívat. (př: pokud bude chtít aplikace používat model "Káčátko", zavolá jeho funkci IncRef. To vyústí v zavolání funkce Create, ve které "Káčátko" aktivuje vazbu se svou texturou, jak jinak než voláním IncRef. A až nyní dojde k samotnému načtení textury do grafické paměti.)
  3. Aplikace již nepotřebuje ten či onen zdroj. Sníží tedy počet odkazů zavoláním DecRef. Ta, pokud byl tento odkaz poslední, zavolá funkci Free, která podstatné části zdroje opět uvolní a sníží počet referencí zdrojům, které tento používal.
  4. Pokud si chce aplikace (tedy případný editor, který není součástí projektu) uchovat informace o daném zdroji, aby ho mohla příště načíst ve stávající podobě, zavolá jednoduše funkci Save, k čemuž není nutné, aby byl tento zdroj "vytvořen". Stejný princip je použit pro uložení celé herní scény.
Funkce Load/Save přijímají jako parametr jednoduché objektové zapouzdření práce se souborem či řetězcem znaků: CCyp3DParser. Jde o triviální rozšíření streamu o načítání, které je vhodné pro funkci použitých skriptů (viz. dále). Každý zdroj ve svém konstruktoru nadefinuje své parametry pomocí třídy CCyp3DParams. Takový parametr má jméno, typ, informaci o nezbytnosti naplnění při načítání zdroje a ukazatel na samotnou datovou položku. Při CCyp3DParams::LoadAll (funkce je volána z funkce CCyp3DResource::Load), se třída CCyp3DParams snaží ze vstupního streamu (přes CCyp3DParser) naplnit tyto parametry. Pokud nedojde k načtení všech nezbytných parametrů, dojde k chybě a načtení se nezdaří.
Ukládání funkcí Save probíhá stejně.

:| Identifikace a umístění zdrojů |:

Každý zdroj je jednoznačně určen svým jménem (char*). V době načítání a tvoření vazeb mezi zdroji, se používá právě tohoto jména k jeho nalezení. Zdroje jsou uloženy ve poli binárních stromů v objektu Keeper. Ten tuto strukturu spravuje, přidává a ubírá prvky a v případě potřeby je likviduje. V skriptech (viz. dále) jsou tyto zdroje referovány právě podle zmíněného jména.

:| Hierarchie zdrojů a herní objekty |:

Již jsem naznačil, jak probíhá práce se zdroji. Teď se budu věnovat tomu, co jsou to zdroje. V tomto projektu je celá myšlenka rozvinuta do ad absurda. Zdroji (tj. potomky zdrojů) jsou textury, meshe, stateblocky, materiály, modely a dokonce i herní objekty. Proč herní objekty? Protože i je je třeba identifikovat a i na ně jsou vytvářeny odkazy.

Mám-li dvě postavičky, které obě útočí na jednoho nepřítele, a jedna z nich ho zabije, je jisté, že nepřítel by měl být uvolněn z paměti. Pokud ho však uvolním hned, druhá postavička bude stále kdesi v sobě mít odkaz na cíl svého útoku, který již nebude platný. A tedy je třeba hlídat, kolik herních objektů na nás ukazuje a uvolnění provést až po snížení počtu odkazů na nulu. K tomu se náramě hodí funkce Free. Zvláštností herních objektů oproti jiným zdrojům, tedy je, že se při zavolání Free dealokují. A tedy na rozdíl od jiných zdrojů, nemohou projít více cykli Create/Free, což se zase hodí na zdůraznění doby existence herního objektu. Pokud aplikace vytváři nějaký herní objekt, zvýší jeho počet odkazů, čímž zapříčiní již popisované pyramidové vytvoření zdrojů, které herní objekt používá. Toto úvodní IncRef může být poeticky chápáno jako vložení "duše" do objektů. Stejně tak při zabití/zničení herních objektů je tato "duše" odejmuta DecRef. Po tomto odejmutí "duše" bude objekt vyřazen z fyzické hierarchie a bude zabírat místo v paměti (patřičně označen za nepoužitelný) jen než budou zrušeny všechny odkazy na něj.

Z hierarchie tříd již plně fungují:

..::|| Herní pole ||::..

O herní pole se strará, jak již bylo řečeno, objekt CCyp3DField ve spolupráci s Céčkovskými funkcemi na hledání nejkratší cesty a viditelnosti. Herní plán je rozdělen na políčka jako základní část. To může být buď volné, dočasně obsazené (někdo na něm stojí) nebo trvale neprůchozí (je tam třeba zeď). Každé políčko má definován svůj vzhled tvarem, texturou a barvou. Tvar vyjadřuje, zda jde o podlahu, stěnu, roh stěny atd., význam textury je jasný a barva je výsledkem výpočtu viditelnosti.

Políčka jsou slučována do sektorů (většinou 8x8 políček). Sektory obsahují VertexBuffery a IndexBuffery, které jsou naplněny trojúhelníky scény patřící na daná políčka. Sektory také obsahují spojové seznamy herních objektů, které sem patří. Takové rozložení urychlí výběr objektů na nějaké pozici tím, že nebude nutné prohledávat všechny herní objekty, což by při větších mapách bylo zbytečně pomalé. Také bude-li nutné přepočítat některá políčka (třeba jejich viditelnost), bude jejich sektor označen jako pozměněný a při příštím updatu dojde k přepočítání jeho políček, které to potřebují (tj. jsou tak označeny).

Část C obsahuje funkce: poslední dvě jmenované funkce problém řeší jednoduchým průchodem do šířky podle obsazenosti políček.

..::|| CCyp3DHWND a CCyp3DModul ||::..

Již jsem popsal, jak vypadají zdroje, herní objekty a herní pole a naznačil jsem jak vypadá hierarchie tříd. Nyní něco z vnějšku. Hlavní třídou aplikace je CCyp3DHWND (její globální instance g_Cyp3DHWND). Úkolem této je inicializovat DirectX, Direct3D, vytvořit okno Windows a to vše učinit podle zadané (načtené ze souboru) konfigurace. To je jedna část práce, taková řekněme obálka, herního cyklu. Inicializace -> Herní cyklus -> Uvonění. CCyp3DHWND také obsahuje funkci herního cyklu, při kterém sleduje zprávy Windows, sleduje zda nedošlo ke ztrátě D3DDevice (viz. DirectX SDK), aktualizuje časovač (globální instance třídy Cyp3DTimer) a pokud je vše v pořádku volá UpDate a Render instance třídy CCyp3DModul.

Úkolem této třídy je práce se samotnými herními objekty a okení hierarchíí. Vkládání objektů do fyzických hierarchií a volání jejich funkcí Update a Render, když je potřeba. Tato třída se zase nestará o práci s nastavením a bere D3DDevice jako fakt. Třída Modulu se také obsahuje instanci Keeperu a disponuje funkcí pro načítání konfiguračních souborů. Další důležitý úkol Modulu je rozesílání zpráv. Funkcí SendMessage je možné zaslat zprávu libovolnému potomku třídy Resource (zdroje, herní objekty, okna) zaregistrovanému v Keeperu nebo samotnému Modulu. V prubehu vyvoje jsem se rozhodl nepoužít pro herní mechanizmi posílání zpráv a tak je sice třída CCyp3DModul připravena rozesílat zprávy všem zdrojům, ale tuto možnost využívá jen okení větev zdrojů. Zbytek se řídí spíše skripty nebo řízení nepotřebuje (viz. dále).
Tento objekt se také stará o načítání a ukládání hry pomocí funkcí Save, LOad a Include (jako load, jen neprovede výmaz již načtených zdrojů a údajů)
Pomocí metody PlaySkript pak přečte z libovolného potomka třídy CCyp3DParser skript a provede všechny jeho příkazy.

..::|| Okení systém ||::..

Jelikož jsem slíbil herní menu, rozhodl jsem se věc neodfláknout a udělat pořádnou okení hierarchii. Třída okna je odvozena od praotce objektů CCyp3Obj (získala tak práci s maticemi). Okna mají své dynamické vertex a index buffery, umí si přeposílat zprávy o stisku a pohybu myši a dědit pozici od svých fyzických rodičů. Existují tyto třídy okeního systému:



..::|| Myš a kurzor ||::..

Ovládání postavy je řešeno pomocí myši. V každém framu je zjišťeno její umístění a tato informace probublává okení hierrachií. Pokud není zpracována, je třeba zjistit, zda hráč ukazuje na nějaký objekt či jen na místo na mapě.

  1. Průsečík s objekty - třída CCyp3DObj zavádí funkce RayTest a RayPick právě pro tento účel. RayTest - dostane vektor a jeho počátek a zjistí průsečík s objektem (tedy s jeho obalovým obdélníkem) RayPick - dostane vektor, jeho počátek a příznaky, a vrací objekt (z hierarchie pod ním), s nejbližším průsečíkem, který má alespoň jeden z příznaků aktivní.
  2. Průsečík s herní mapou pro nalezení souřadnic, kam hráč ukazuje, je řešen jednoduchým průsečíkem s herní rovinou a dopočítáním souřadnic.
Když zjistíme, kde se myš nachází, nastavíme kurzor na příslušný model, podle toho, jaké akace je předmět pod kurzorem schopen

..::|| Skripty ||::..

Při zavádění herních principů pro tvorbu samotného příběhu, rozhovorů a různých přepínačů, bylo využito velice triviálního skriptovacího jazyka, který rozumí několika málo příkazům: Skripty jsou velice jednoduché a neumožňují žádnou aritmetiku. Jde čistě jen o co nejjednodušší umožnění řízení hry. Při nešetrném používání skriptů lze docílit běhové chyby programu. Skripty nejsou určeny pro úpravu uživatelem a tak nedochází k typové kontrole (tedy pokud pouzivate nejaky zdroj urcitym zpusob v programu dojde k hrubemu pretypovani a je tedy mozne vyloudit takto behovou chybu).

Tvorba dokonaleho a typově bezpečneho skriptovacího jazyka je mimo rámec tohoto projektu, a tak ani k ničemu takovemu nedošlo. Hlavním cílem bylo zajistit princip, který by byl použitelný, nikoli dokonalý.

Skripty se spouštějí v různých situacích. Jednak se pomocí skriptů načítá celá herní scéna a při volbě uložit hru a načíst hru se vlastně jen celá hra opět převede na původní skript resp. se tento skript spustí. Dále má každý herní objekt možnost reagovat na 4 události: Tyto 4 případy pokrývají potřebnou škálu a umožňují naskriptování dostatečného množství efektů pro hru.