Správa rozsáhlých herních dat
Z CHWiki
Obsah |
[editovat] Správa rozsáhlých herních dat
Složitost her roste a s ní roste i objem nemultimediálních dat. Zatímco dříve se hra skládala v podstatě jen z textur, modelů, zvuků a levelů, dnes k tomu přibyly megabajty dat popisujících chování, úkoly v misi, vlastnosti objektů, vývojové stromy, rozložení GUI, RPG charakteristiky, shadery a spousty dalších informací. Tato data je třeba nějak organizovaně vytvářet a především s nimi pracovat v programu. V tomto článku bych chtěl poukázat na některé problémy, na které můžete narazit a nastínit možná řešení. Čerpám ze svých zkušeností s vývojem hry UFO: Extraterrestrials a z obecných programátorských znalostí :)
Pro ilustraci, celkový objem textových souborů v UFO:ET s popisy všech dat dělá asi 1 MB, uložená hra v misi má objem přes 2 MB. To je docela dost informací a přitom UFO:ET je jen středně velká hra.
U projektů, ve kterých je víc jak 10 druhů jednotek, víc jak 10 zbraní, nějaký ten vývojový strom a podobně už není příliš dobrý nápad všechno natvrdo zadrátovat do kódu a to ani v případě, že používáme skriptovací jazyk. Lepší přístup je mít co možná nejvíce dat v externích souborech a v programu mít jen příslušné chování (koneckonců takové je prvotní poslání počítačů). Tento přístup se nazývá table-driven programming. Usnadní to dost opakující se práce při programování, umožní to snadnější modifikaci a ladění game designu a především bude designer schopen měnit si vlastnosti herních objektů bez pomoci programátora, což ušetří opět dost času programátora (za předpokladu, že designér bude schopen se v datových souborech vyznat, což není úplně samozřejmost).
[editovat] Typy dat a ukázky
Čím komplikovanější hra, tím více herních dat a nejkomplikovanější jsou obvykle hry strategického rázu. O jaká konkrétně data se tedy jedná? Ukážeme si několik příkladů, které by se mohly vyskytnout ve strategické hře:
zbraně
plasmová_puška
// strategické vlastnosti
cena_kov 4300
cena_energie 9800
doba_vyroby 200
obouruční ano
minimální_síla 45
// vzhled předmětu
ikona "images\gui\weapons\plasma_rifle.png"
model "models\weapons\plasma_rifle.mdl"
střela
// bojové vlastnosti střely
ničivost 90
ničivost_rozptyl 20
přesnost 20
frekvence_střílení 2
// grafické vlastnosti střely
rychlost_letu 0.4
velikost_x 0.2
velikost_y 0.6
velikost_z 0.3
model "models\bullets\plasma.mdl"
let_efekty
[1]
typ EFEKT_ČÁSTICE
soubor "particles\bullets\plasma_fly.txt"
náraz_efekty
[1]
typ EFEKT_CASTICE
soubor "particles\bullets\plasma_explosion.txt"
[2]
typ EFEKT_ZVUK
soubor "sounds\bullets\plasma_explosion.wav"
[3]
typ EFEKT_STOPA
soubor "images\decals\plasma_hit.png"
[4]
typ EFEKT_SVĚTLO
barva 255,12,45
jednotky
těžký_robot
// strategické vlastnosti
cena_kov 22100
cena_energie 35000
doba_vyroby 2000
...
model "models\units\heavy_robot.mdl"
létá false
zničený_model "models\units\heavy_robot_destroyed.mdl"
rychlost 0.01
obrana 78
zvuky
klid "sounds\units\heavy_robot\idle.wav"
pohyb "sounds\units\heavy_robot\moving.wav"
smrt "sounds\units\heavy_robot\death.wav"
zbraně
[1] "plasmová_puška"
[2] "naváděná_raketa"
výzkum
výzkum_kořen
cena_kov 0
cena_energie 0
doba_výzkumu 0
zpřístupňuje_výzkum
[1] "výzkum_plasmová_puška"
[2] "výzkum_vylepšení_brnění"
zpřístupňuje_předměty
[1] "brnění_základ"
[2] "konvenční_puška"
...
výzkum_plasmová_puška
cena_kov 0
cena_energie 7000
doba_výzkumu 100
zpřístupňuje_výzkum
[1] "výzkum_těžký_robot"
zpřístupňuje_předměty
[1] "plasmová_puška"
Na první pohled to může vypadat složitě, ale po přečtení budete jistě souhlasit, že je to vlastně samozřejmost. Zbraně,
jednotky a stromy výzkumu ale nejsou jediná data, která můžeme ve hře mít. V UFO:ET je například encyklopedie všech
předmětů, lodí, budov a mimozemšťanů. Předměty jsou uspořádány do kategorií a u každé položky je popis, obrázek a seznam
vlastností. Samozřejmě seznam vlastností je u každého předmětu jiný a proto i tyto seznamy vlastností včetně rozdělení
do kategorií jsou součástí herních dat. Jistě si dokážete představit i další data jako například šablony pro vytváření
nových objektů, konstanty pro bojový systém, RPG vlastnosti a tak dále. Pokud k tomu přidáme ještě uložené hry, ve
kterých se míchá několik zdrojů a druhů dat dohromady, dostanete z toho pěkný guláš. Úkolem programátora při tvorbě
hry je se v tomto guláši vyznat :)
Herní data můžeme rozdělit na několika málo kategorií podle toho, jak se v průběhu hry mění.
- Přesně dané hodnoty, které se během hry nemění a nemají více instancí. Asi všechna data uvedená v příkladech patří do této kategorie.
- Hodnoty konstantní pro jednu hru (období od stisknutí tlačítka "Nová hra" po výhru). Sem by se daly zařadit například vlastnosti postavy v RPG hře jako síla, inteligence, obratnost a podobně.
- Hodnoty, které se v průběhu hry mění, ale v rámci mise jsou konstantní. Například seznam výsledků absolvovaných misí, v některých případech počet vojáků nebo vybavení (pokud je nezískáváme v misi).
- Hodnoty, které se mění i v průběhu mise jako například počet nábojů ve zbrani, množství peněz a další.
Samozřejmě toto rozdělení je třeba příslušně modifikovat podle druhu hry a jejích herních mechanismů. Vypsal jsem spíš více kategorií, abyste nemuseli vymýšlet další.
Samostatnou kapitolou jsou uložené hry. Pokud už máme nějaký systém v herních datech, mechanismus ukládání a načítání hry ho bude nejspíš používat. Vlastnosti objektů se potom rozdělí na ty, které jsou statické a na ty, které se mění a ukládají se.
[editovat] Formát datového souboru
Data mohou být buď binární nebo textová. V prvním případě je nutné naprogramovat editor, což je asi dost zbytečné práce navíc, protože ve většině případů by textová reprezentace a váš oblíbený programátorský editor bohatě stačil. Specializovaný editor herních dat může pracovat i s daty v textové podobě. Navíc textové soubory jsou přátelštější s programy pro správu zdrojových kódů (SVN, CVS, ...). Dále se tedy budeme zabývat jen textovým formátem.
Abstraktní data mají typicky podobu grafu (příklad je na obrázku 1). V uložené textové podobě je ale lepší stromová struktura s odkazy a tato struktura by měla být pokud možno přehledná a intuitivní.
Obrázek 1. Příklad grafu objektů ve hře. Modré obdélníčky jsou objekty, elipsy jsou jejich vlastnosti. V tomto grafu jsou zkombinovaná statická data (informace o zbranich jako např. plasmová_puška) s dynamickými daty (stav hry v objektu herní_info).
Odkazy ve stromové struktuře můžeme buď nechat v textové formě a odkazovaný objekt vždy vyhledat až v okamžiku, kdy
ho potřebujeme, nebo je můžeme automaticky nahradit skutečnými odkazy ukazujícími na existující instance. První
přístup je vhodnější pro statické informace, druhý přístup se hodí pro ukládání/načítání hry, což jsou obvykle
dynamická data s mnoha odkazy.
Přístup, kdy se celý objektový graf načte automaticky, má ale několik skrytých problémů:
- Požadavek na načtení jednoho malého nevýznamného objektu může díky odkazům způsobit načtení celého grafu. To někdy může vadit, někdy nemusí. Pro větší přehlednost můžeme preferovat oddělení statických informací jako informace o zbrani od dynamických informací (co má zrovna voják v ruce a kolik v tom je nábojů).
- Pokud budeme takový objektový graf i automaticky ukládat, vzniklé soubory už nebudou mít takovou pěknou strukturu, jako při použití stromu s pevně danou strukturou. Popravdě řečeno to bude spíš docela bordel. Sice můžeme předpokládat, že ukládané soubory nikdo editovat nebude, ale i tak se na ně budeme muset dívat minimálně při programování, testování a opravování chyb.
- I přes všechnu sofistikovanost automatického načítání se najdou informace, které mají smysl jen po dobu běhu hry a je zbytečné je ukládat. Nějaké ruční práci se tedy stejně nevyhneme.
- S předchozím bodem souvisí problém s ukládáním implementačních detailů. Pokud si nedáme pozor a pečlivě nespecifikujeme, co přesně se ukládá (což se může lehko stát, když to s použitím automatického ukládání funguje snadno a rychle napoprvé), budeme mít v uložených datech příliš mnoho implementačních detailů a to značně zkomplikuje kompatibilitu s dalšími verzemi, protože tam se pravděpodobně nějaké implementační detaily změní.
Na druhou stranu pokud zamítneme automatické načítání obsahu odkazů a místo toho ho implementujeme ručně, objekt po objektu a bez rozmýšlení, dostaneme všechny uvedené nevýhody při mnohem větší spotřebě práce. Je třeba strukturu dat dobře rozmyslet a rozhodnout se, co bude odkazovat nepřímo a kde se automaticky načte obsah odkazu.
Ke specifikaci vlastností k uložení a k typům se vrátím později.
[editovat] Existující textové formáty pro ukládání dat
Jak jsme se již zmínili, použijeme textový formát. I tady máme ale na výběr. Můžeme použít XML, vlastní formát nebo nějaký jiný standardizovaný formát.
XML je v poslední době populární formát, existuje spousta parserů, editorů a dalších nástrojů. Vedle toho má dobře vyřešené jmenné prostory, vkládání jednoho typu dat do jiného a podobně. Standardy jako XPath, XQuery, XML Schema a další se mohou také hodit. Odvrácená strana mince je ale jistá složitost celého toho mechanismu a jistá "ukecanost" jazyka samotného aneb "kdo má proboha psát ty miliony špičatých závorek?".
YAML je další možnost. Oproti XML je tento formát mnohem kompaktnější a o něco přehlednější. Dokument v YAML může vypadat například takto:
zbraně:
plasmová_puška:
cena_kov: 4300
cena_energie: 9800
střela:
ničivost: 90
náraz_efekty:
- typ: EFEKT_CASTICE
soubor: "particles\bullets\plasma_explosion.txt"
- typ: EFEKT_ZVUK
soubor: "sounds\bullets\plasma_explosion.wav"
Pokud používáte ve hře nějaký skriptovací jazyk, je možné použít jeho nativní syntaxi. Tím se ušetří implementace parsujícího kódu. Například Homeworld 2 ukládá hru do nativního formátu skriptovacího jazyka Lua.
Poslední možnost je samozřejmě napsat si vlastní formát. V UFO:ET jsme měli vlastní formát, který vypadal podobně jako příklad na začátku nebo jako YAML. Díky použití odsazování a funkce skládání textu podle odsazení v textovém editoru byla práce s ním poměrně snadná.
[editovat] Užitečné funkce na úrovni datového souboru
[editovat] Výchozí hodnoty
Stejně jako v programování není dobrý nápad psát kód stylem copy & paste, i v případě popisu herních dat je dobré vyhnout se duplicitám. Výchozí hodnoty pro určité třídy objektů mohou ušetřit dost psaní a to hlavně v případě, kdy máme mnoho vlastností. Popis objektu, který se skládá jen z odlišností od výchozího nastavení může být také přehlednější.
Na úrovni vlastností objektů lze použít například následující struktury. Každá z nich má své výhody, ale i jisté nevýhody.
První struktura využívá speciální sekci [default].
zbraně
[default]
rychlost_letu 0.4
velikost_x 0.2
velikost_y 0.6
velikost_z 0.3
střela
typ SVĚTLO_BODOVÉ
plasmová_puška
velikost_x 0.3
střela
barva 12, 45, 85
Všechny vlastnosti objektů pod úrovní "zbraně" berou hodnoty ze speciální sekce [default] a pokud je vlastnost uvedena přímo u objektu, použije se místo výchozí hodnoty. Hledání výchozích hodnot se provádí rekurzivně, takže v sekci [default] je něco jako obraz sekce zbraně. Tento přístup se dá poměrně přímočaře přeložit i do XML a je nezávislý na tom, ze které cesty začnete vytvářet objekt. S tímto přístupem jsme schopni umístit výchozí hodnoty jak blízko, tak daleko od skutečného objektu. V zájmu snažší editace a prohlížení dat je lepší dát výchozí hodnoty co nejblíže skutečnému objektu anebo někam, kde se dají snadno a rychle najít.
Další možnost by vypadala asi takto:
zbraně
rychlost_letu 0.4
velikost_x 0.2
velikost_y 0.6
velikost_z 0.3
plasmová_puška
velikost_x 0.3
Zde jsou výchozí vlastnosti na stejné úrovni jako objekty. To může představovat problém pokud byste chtěli vytvořit skriptový objekt z uzlu zbraně, protože by se do něj vložily i výchozí hodnoty a to nechceme. Pokud bychom zadali pro načtení cestu zbraně.plasmová_puška, tento problém bychom už neměli. Tento přístup nám neumožní definovat výchozí hodnoty pro podsekce plasmová_puška.
A nakonec poměrně přímočaré řešení s použitím odkazu:
výchozí_hodnoty
zbraně
rychlost_letu 0.4
velikost_x 0.2
velikost_y 0.6
velikost_z 0.3
zbraně
plasmová_puška *výchozí_hodnoty.zbraně
velikost_x 0.3
Pokud budou výchozí hodnoty mimo hlavní strom objektů, nebudou vadit při vytváření objektů z libovolné cesty. Na druhou stranu tak není možné výchozí hodnoty umístit blízko skutečnému objektu.
Nějakou zabudovanou podporu pro odkazování má jazyk YAML. Doporučuji prostudovat dokumentaci nebo jednoduchý příklad na wikipedii http://en.wikipedia.org/wiki/YAML
[editovat] Seznamy a výčtové typy
Jak bylo vidět v příkladu na začátku, v herních datech se často vyskytují seznamy různého druhu. Čtečka našeho vybraného formátu by je měla umět automaticky rozpoznat a vytvořit z nich nativní seznam v našem programovacím jazyce.
Vedle klasických typů jako je int, float, string je vhodné implementovat i výčtové typy (enum). Využijí se poměrně často a nahrazení výčtového typu textovým řetězcem nebo dokonce (!) číslem je značně náchylné na chyby.
[editovat] Použití dat ze souboru
Jak nyní data načteme a použijeme v programu? Pokud máme ve hře skriptovací jazyk, pravděpodobně to využijeme a vytvoříme z dat přímo skriptové objekty. Ty mohou sloužit jako pouhá úložiště dat, ze kterých si oddělený kód kdykoliv vytahuje to, co potřebuje. Druhá možnost je data deserializovat do "živých" objektů. Ve druhém případě musíme zajistit, aby objekt po deserializaci správně inicializoval ty své součásti, které nejsou do souboru ukládány (například tak, že je vypočítá z ukládaných hodnot). V případě Javy nebo C# můžeme použít reflexi a z dat přímo vytvořit instanci vybrané třídy. Opět musíme zbytek objektu správně inicializovat. Pokud programujeme jen v C++ a skriptovací jazyk ve hře není, je situace už o něco těžší. Jedna možnost je načíst data do seznamů. To je poměrně jednoduché, ale i trochu nepohodlné. C++ je ale mocný jazyk a s použitím vhodných maker nebo šablon a trochy práce lze dosáhnout toho, aby bylo možné k proměnným přistupovat jak běžným způsobem, tak pomocí jejich stringových identifikátorů (a to se využije při čtení souboru).
Pokud máme ve hře schopnou konzoli a používáme skriptovací jazyk, je možné ladit herní konstanty přímo ve hře (za předpokladu, že máme z konzole přístup k objektu, ze kterého se konstanty opakovaně čtou). Typicky trvá spuštění hry nějakou dobu a tato funkce tak může pomoci hru lépe vyladit.
[editovat] Flexibilní skládání chování
Asi vám neuniklo, že efekty nárazu výstřelu z plasmové pušky v příkladu nahoře jsou definovány trochu neobvyklou formou. Tradiční popis efektů výbuchu by vypadal asi takto:
náraz_částice "particles\bullets\plasma_explosion.txt" náraz_zvuk "sounds\bullets\plasma_explosion.wav" náraz_stopa "images\decals\plasma_hit.png"
a podle toho by vypadal i obslužný kód. Toto řešení je sice velice jednoduché, ale není příliš flexibilní ani elegantní. Oproti tomu řešení efektů nastíněné v úvodním příkladu je mnohem flexibilnější. Funguje to tak, že datový soubor definuje všechny objekty, které se podílejí na tvorbě efektu. Při nárazu střely v kódu vytvoříme všechny objekty a každý z nich už potom na místě výbuchu odehraje svoje divadlo. Takový mechanismus není příliš náročný na programování a poskytuje mnohem větší flexibilitu. Navíc stejné objekty můžeme použít i pro efekt letící střely a v dalších případech.
[editovat] Využití polymorfismu
Pokud používáme vytváření živých objektů z herních dat, můžeme s výhodou použít dědičnost a polymorfismus k dosažení dalšího stupně flexibility. V datovém souboru určíme, která třída se má vytvořit a tím ovlivníme, jak se daný objekt bude chovat. Například ničivé chování zbraně může být buď standardní:
plasmová_puška
ničivost
className TPřímýZásahNičivost
ničivost 30
ničivost_rozptyl 5
nebo můžeme mít plošný účinek:
těžká_raketa
ničivost
className TPlošnýVýbuchNičivost
poloměr 4
ničivost_uprostřed 100
Podobně způsob pohybu střely může být implementován třídami jako TPřímýLet, TVrženýObjekt, TŽádnýLet nebo TNaváděnýLet.
[editovat] Poznámka ke kategorizaci
Pokud se objekty ve hře nějakým způsobem spravují (prodávají, nakupují, vyrábějí, předávají mezi vojáky a podobně), pravděpodobně vznikne potřeba objekty zobrazovat v seznamu. Některé objekty ale do jednoho druhu seznamu patří a jiné zase ne a naopak. Přímočaré řešení je takové, že objektu nastavíme "druh" a v příslušném seznamu vypisujeme jen objekty s danými druhy. Toto řešení je ale nevýhodné pokud se kategorie překrývají, což se dá očekávat. Lepší je tedy objektu přiřadit několik vlastností typu ano/ne a každou vyplnit podle toho, zda ji splňuje, nebo ne. Tak máme mnohem větší volnost v zařazování objektu a nemusíme vytvářet exponenciálně mnoho kategorií jako "JednoručníZbraněNaBlízkoAtreidi", "ObouručníZbraněNaDálkuFremeni" a podobně. Podmínka pro všechny zbraně klanu Atreidů by pak vypadala asi takto:
(kategorie == JednoručníZbraněNaBlízkoAtreidi) || (kategorie == ObouručníZbraněNaBlízkoAtreidi) || (kategorie == JednoručníZbraněNaDálkuAtreidi) || ... // dalších 8 možností
A každého asi napadne, co se stane, když přibude další volba...
Místo toho bude
jednoruční ano na_blízko ne klan KLAN_Atreidi
a i dotazy budou vypadat lépe:
(klan == KLAN_Atreidi)
[editovat] Validace a typování
Nejjednodušší přístup ke čtení herních dat nic nekontroluje a prostě přehází všechny vlastnosti ze souboru do paměti. Čím více dat ale máme v externím souboru, tím je více prostoru k vyjádření mají chyby. A pokud soubor edituje i někdo jiný než jeho původní návrhář, můžeme si počet chyb vynásobit dvěma. Proto se hodí nějaký mechanismus, který v co největší míře soubory kontroluje a upozorní nás jakmile na chybu narazí (tedy při načítání). Tak se dozvíme, kde a co je špatně místo toho, abychom půl dne strávili hledáním důvodu, proč zbraň nefunguje.
Abychom mohli vstupní data kontrolovat, je třeba mít k dispozici nějaký popisný mechanismus, kterým popíšeme, co si od těch dat představujeme. V případě XML můžeme sáhnout po XML Schema, Relax NG, DTD nebo jiném standardizovaném mechanismu a pak máme zadarmo i validující parser. V případě jiných formátů musíme validaci pořešit vlastnoručně. Můžeme použít stejný mechanismus, který se uplatňuje v silně typovaných jazycích. Nadefinujeme typy (třídy) a k nim seznam vlastností včetně očekávaných typů. V herních datech pak bude u každého objektu určen typ, podle kterého se má validovat. Navíc pokud z herních dat vytváříme přímo živé objekty, tato definice určuje třídu živého objektu.
Příklad definice typů:
TZbraň
cena_kov int
cena_energie int
doba_výroby int
obouruční bool
minimální_síla int
ikona string // pokud si vymyslíme vlastní typ path, můžeme kontrolovat i to, zda soubor existuje
střela TStřela
typ TTypZbraně
TTypZbraně enum
[1] ruční
[2] psionická
[3] konvenční
TStřela
ničivost float
přesnost float
Abychom si ušetřili práci, definici typů můžeme nadefinovat ve stejném formátu (XML, YAML, ...) jako vlastní data.
Dá se očekávat, že typy objektů budou mezi sebou intuitivně mít vztah dědičnosti. To je vhodné nějakým způsobem v definici zachytit.
Jakmile při čtení narazíme na chybu, musíme vypsat v jakém souboru, na jakém řádku se chyba nachází a co konkrétně je špatně a to nejlépe tak, aby to pochopil i designér. Pokud to nepochopí, můžeme od něj očekávat mnoho zpráv na vašem oblíbeném IM.
[editovat] Na co si dát pozor
- Identifikátory předmětů by neměla být čísla, ale zapamatovatelný řetězec ("plasmaRifle" je mnohem lepší než p38).
- Pokud vaše hra podporuje více druhů nábojů v jedné zbrani, potom je většina informací o letu a dopadu střely svázána s nábojem a ne se zbraní. To může být na první pohled trochu neintuitivní.
- Pokud má s datovými soubory pracovat designér, je třeba počítat s tím, že je to osoba do programování a vašeho formátu nezasvěcená a proto je třeba mu to pokud možno srozumitelně vysvětlit. Dále je vhodné zdokumentovat každou datovou položku, třeba pomocí komentářů přímo v souboru.
[editovat] Závěr
Ukázali jsme si, jak asi vypadají data ve složitějších hrách, jak se dají ukládat do souboru. Představili jsme pár možností, jak tato data ukládat, jak je číst a jak s nimi pracovat. Dále jsme se zmínili o pár metodách, které mohou práci s herními daty zjednodušit a zpříjemnit. Některé metody mají své klady, některé své zápory, proto je třeba vždy zhodnotit, co je ve vašem případě nejvýhodnější.

