----------
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:
- Je třeba zajistit vazby mezi objekty, které nějaký zdroj potřebují a mezi již existujícími zdroji (zabránění duplikaci)
- V případě neužívání zdroje, je vhodné tento uvolnit (nebo alespoň jeho část v grafické paměti)
- 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í:
- 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ů.
- 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.)
- 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.
- 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í:
- Resource - praotec všech zdrojů
- StateBlock - jednoduché zapzdření D3DStateBlocku
- Texture - jednoduché zapzdření D3DTextury
- Mesh - jednoduché zapzdření D3DMeshe
- BaseModel - Praotec modelů, definuje virtuální funkce pro zobrazení modelu na dané pozici, práci s animací a
pomocné funkce pro načítání informací o použitých materiálech.
- MeshModel - Jednoduchý model bez animace používající Mesh
- Xerk - Animace postavičky
- SwitchModel- Model objektu s dvěmi polohami. Využit pro reprezentaci dveří a pák
- Material - Praotec materiálů, definuje virtuální funkci pro použití materiálu ApplyMaterial
- MonoMaterial - Materiál s jednou texturou
- Object - Praotec hmotných objektů, stará se o práci s maticemi a zakládá fyzickou hierarchii objektů
- Camera - Objekt kamery, který nastavuje vlastnosti projekce a směr pohledu. Také sleduje zaměřený objekt
- GameObject - Herní objekt, přeplněná třída obsahující logiku ovládání jednotek, práci s mapou
a trochu inteligence. Také obsahuje herní data jako životy, rychlost atd.
- Monster - Potomek GameObjectu, ktery pridava jen minimum uprav. Jde o objekt Nestvury,
utocici na hrace po priblizeni.
- Door - Potomek GameObjectu reagující na zprávu OnSwitched změnou průchodnosti
- Item - Další potomek GameObj. Tentokrat jde o herní objekt, který po použití zmizí. Ve hre samotné
je využit jen pro reprezentaci "healingu".
- Field - Herní pole. Stará se o udržování, načítání, zobrazování a update herního pole.
..::|| 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:
- Map_Create - alokace a inicializace struktury mapy
- Map_Load - pro načtení mapy ze souboru
- Map_Save - pro uložení mapy do souboru
- Map_Reload - znovunačtení mapy (+dealokace původně načtené mapy)
- Map_FindWay - najde nejkratší cestu mezi dvěma body a pokud je cíl obsazen,
snaží se dostat na sousední políčko. Funkce vrací spojový seznam
políček a úhlů natočení procházející postavičky
- Map_Visibility - řešení viditelnosti, projde políčka dosažitelná určitým počtem kroků a jejich "budoucí
barvu" nastaví podle vzdalenosti, kterou je k nim nutno urazit. Samotná světlost políček
je pak každý frame plynule přibližována teto "budoucí barvě", čímž vznikne žádaný dojem
postupného odkrývání mapy.
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:
- Window - Praotec oken, umožňuje práci s pozicí okna a rozposílání zpráv
- Text - Nereaguje na zprávy, ale umí naplnit dynamické buffery meshem pro zadaný řetězec znaků s triviální
možností formátování. Konec řádky funguje tak jak se očekává a tagy {b,{w,{g,{Y,{G,{R,{B lze měnit barvu textu
- Button - Jednoduché tlačítko posílající svému rodiči informaci o stisknutí
- Frame - Objekt zapouzdřující rámec okna. Tento rámec lze uchopit a táhnout za horní lištu
- InfoWnd - Okénko na zobrazení poctu zivotů a informací o počtu snímků za vteřinu
- Menu - Okno herního menu, reaguje na zprávy od tlačítek
- Dialog - Okno uzpůsobené k přehrávání rozhovorů
..::|| 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ě.
- 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í.
- 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:
-
create jméno_třídy jméno_nově_vznikajícího_zdroje ( parametry pro tvorbu zdroje )
-
load_stateblocks - vytvorí D3D StateBlocky pro nastavení vlastností materiálů
-
load_field - načte herní pole
-
create_field - vytvoří načtené herní pole
-
include - vloží na toto místo skript z daného souboru
-
place_object - umístí daný objekt na urcitou pozici na herním poli
-
set_player - nastaví určitý herní objekt jako hráče (bude ovládán uživatelem a po jeho smrti hra skončí)
-
kill - odstraní objekt z herní plochy
-
place_window - umožní umístění okna do hierarchie oken
-
place_window_center - umístí okno do hierarchie oken a vystředí vzhledem k rozměrům obrazovky
-
play_dialog - zobrazí okno rozhovoru s možnostmi odpovědí a skriptů spuštěných při jejich volbě. Po
tomto příkazu skript normálně pokračuje a následující příkazy jsou vykonány hned, nikoli až po konci rozhovoru.
-
switch - umožňuje přepnutí objektů, které toto podporují. Jedná se o dveře různých typů a páky.
-
set - umožňuje nastavení jedné z 128 hodnot pevných pro rozehranou hru (jednoduchá náhražka globálních proměnných)
-
if - podle nastavení jedné ze 128 hodnot je možné provést větvení skriptu
-
set_flags - umožnuje změnit příznaky zvoleného objektu
-
load_level - načtení zvolené úrovně ze souboru
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:
-
OnKilled - Pokud byl zabit. (Ještě před animací úmrtí a před úplným zničením zdroje)
-
OnSpeak - Pokud byl objekt herní postavou vybídnut k rozmluvě
-
OnUsed - Pokud byl objekt herní postavou použit
-
OnSwitched - Pokud byl objekt přepnut příkazem switch.
Tyto 4 případy pokrývají potřebnou škálu a umožňují naskriptování dostatečného množství efektů pro hru.