České vysoké učení technické v Praze Fakulta elektrotechnická Katedra počítačové grafiky a interakce
Bakalářská práce
Rozhraní pro tvorbu plošinových her Karel Mačalík
Vedoucí práce: Ing. Michal Hapala
Studijní program: Softwarové technologie a management, Bakalářský Obor: Web a multimedia 20. května 2011
iv
v
Poděkování Rád bych upřímně poděkoval Ing. Michalu Hapalovi za vedení práce, cenné rady a připomínky a v neposlední řadě za stanovení termínů, díky kterým jsem práci dokončil včas. Dále bych rád poděkoval své rodině za podporu během celého studia.
vi
vii
Prohlášení Prohlašuji, že jsem práci vypracoval samostatně a použil jsem pouze podklady uvedené v přiloženém seznamu. Nemám závažný důvod proti užití tohoto školního díla ve smyslu §60 Zákona č. 121/2000 Sb., o právu autorském, o právech souvisejících s právem autorským a o změně některých zákonů (autorský zákon).
V Králíkách dne 19. 5. 2011
.............................................................
viii
Abstract Computer games are an important part of the entertainment industry, however their development can be rather complex and expensive. The aim of this work is to describe an effort to create tools for developing simple platform games – game engine and editor. The beginning of this thesis briefly aknowledges reader with computer games and nowadays development tools and compares current commercial and free solutions. Then it continues with examination of technologies for game engine and editor development. Both Design and Implementation chapters provide insight into complicated development process with usage of modern programming paradigms such as event driven development, multithreading or scripting support. Included DVD contains developed application prototype, which is being described within this document.
Abstrakt Počítačové hry tvoří důležitou část zábavního průmyslu, ale jejich vývoj může být velice složitý a nákladný. Cílem této práce bylo popsat úskalí vývoje nástrojů pro usnadnění tvorby jednoduchých plošinových her – herního enginu a editoru. Práce začíná úvodem do problematiky počítačových her a rozborem současných nástrojů pro jejich vytváření, porovnává současná komerční i zdarma distribuovaná řešení. Dále pokračuje průzkumem aktuálních technologií pro vývoj herního enginu a editoru. Návrhová a implementační část přináší náhled do leckdy komplikovaného procesu vývoje s využitím moderních programovacích prostředků, jako je událostmi řízené programování, podpora více vláken nebo implementace skriptování. Na přiloženém DVD je prototyp výsledné aplikace, jejíž vývoj je v práci popisován.
ix
x
Obsah 1 Úvod 1.1 Co je to hra . . . . . . . . . . 1.2 Jak hry dělíme . . . . . . . . 1.2.1 Logické hry . . . . . . 1.2.2 Adventury . . . . . . . 1.2.3 Akční hry . . . . . . . 1.2.4 Strategické hry . . . . 1.2.5 RPG (hry na hrdiny) 1.2.6 Rodinné a casual hry . 1.2.7 Sportovní hry . . . . . 1.2.8 Plošinové hry . . . . . 1.3 Kdo hry vytváří a jak . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
2 Nástroje pro tvorbu her a cíl BP 2.1 Herní engine . . . . . . . . . . . . . 2.2 Editor . . . . . . . . . . . . . . . . 2.3 Open source a low-cost alternativy 2.4 Technologická špička . . . . . . . . 2.5 Cíl práce . . . . . . . . . . . . . . . 3 Analýza technologií 3.1 Programovací jazyky . 3.2 Renderovací knihovny 3.3 Fyzikální knihovny . . 3.4 Skriptování . . . . . .
. . . .
. . . .
. . . .
. . . .
4 Návrh aplikace 4.1 Událostmi řízená aplikace . . 4.2 Systémy . . . . . . . . . . . . 4.3 Herní smyčka a třída Game . 4.4 Graphics a ThreadedGraphics 4.5 Task Manager . . . . . . . . . 4.6 State a StateController . . . . 4.7 Načítání a ukládání dat . . . 4.8 Struktura levelu . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . xi
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . .
1 1 2 2 2 3 3 3 3 3 4 4
. . . . .
7 7 8 9 11 12
. . . .
15 15 16 16 17
. . . . . . . .
19 19 21 22 23 24 25 26 26
xii
OBSAH
4.9
Editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5 Popis implementace 5.1 Reference counting . . . . . . . . . . . . . . . . . . . 5.2 Výjimky . . . . . . . . . . . . . . . . . . . . . . . . . 5.3 Herní jádro, smyčka . . . . . . . . . . . . . . . . . . 5.4 Graphics . . . . . . . . . . . . . . . . . . . . . . . . . 5.5 Zamykání (thread-safeness) . . . . . . . . . . . . . . 5.6 Binární serializace objektů a její záludnosti . . . . . 5.7 Fyzika Bullet . . . . . . . . . . . . . . . . . . . . . . 5.8 Umělá inteligence . . . . . . . . . . . . . . . . . . . . 5.8.1 Vyhledávání cesty v prostoru, A* algoritmus 5.9 Skriptování a jazyk Lua . . . . . . . . . . . . . . . . 5.9.1 Správa zásobníku . . . . . . . . . . . . . . . . 5.9.2 Boxing objectů . . . . . . . . . . . . . . . . . 5.9.3 Eventy ve skriptech . . . . . . . . . . . . . . 5.9.4 Debugger a krokování v editoru . . . . . . . . 5.9.5 Ukázka skriptování . . . . . . . . . . . . . . . 5.10 Mezivrstva C++/CLI . . . . . . . . . . . . . . . . . 5.11 Komunikace Engine / Editor . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
29 29 30 31 31 32 33 35 35 36 38 38 39 41 42 43 43 44
6 Závěr
45
A Výpisy kódu
49
B UML diagramy
53
C Obrázky
57
D Instalační a uživatelská příručka 59 D.1 Spuštění hry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 D.2 Spuštění editoru . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 E Obsah přiloženého DVD
61
Seznam obrázků 1.1
Plošinová hra Trine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1 2.2 2.3
Rozhraní editoru UnrealEd . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Game Maker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Editor Hammer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
4.1 4.2 4.3 4.4 4.5 4.6 4.7
Třídy související s událostmi . . . . . . . . . . . . . . . . . . SystemManager sdružuje veškeré systémy . . . . . . . . . . Herní smyčka událostmi řízeného enginu . . . . . . . . . . . Životní cyklus vícevláknového rendereru ThreadedGraphics Manažer stavů a zpracování vstupu . . . . . . . . . . . . . . Třídy pro ukládání a načítání dat . . . . . . . . . . . . . . . Rozvržení rozhraní editoru . . . . . . . . . . . . . . . . . . .
5.1
Editor obsahuje debugger pro krokování kódu . . . . . . . . . . . . . . . . . . 42
B.1 B.2 B.3 B.4
Třída Game . . . . . . . . . . . . . Rozhraní Graphics . . . . . . . . . Struktura TaskManageru . . . . . Struktura základních LevelObjektů
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
4
20 21 23 24 25 26 27
53 54 54 55
C.1 Úprava cest AI v editoru . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
xiii
xiv
SEZNAM OBRÁZKŮ
Kapitola 1
Úvod V dnešní době tráví lidé čím dál více času konzumací multimediálního obsahu pomocí počítačů, mobilních telefonů, tabletů či herních konzolí. Moderní člověk 21. století tráví velkou část volného času u svého osobního počítače prohlížením internetových stránek, poslechu hudby, sledováním filmů a hraním počítačových her. Právě počítačové hry zabírají větší a větší část. Podle průzkumu hrálo v roce 2007 nějakou počítačovou hry 72% Američanů[1]. V roce 2010 byl průměrný věk hráčů 31 let[2] a v průměru strávili hraním 13 hodin týdně[2]. V roce 2010 utratili Američané za počítačové hry více než 15 miliard dolarů[3]. Ve Velké Británii jsou tržby z her větší než tržby z filmů[4]. Ani Česká republika není v tomto ohledu příliš pozadu – obrat herního trhu České a Slovenské republiky dosáhl v roce 2010 rekordní výše 2,189 miliardy korun[5]. Počítačové a konzolové hry evidentně tvoří velký byznys, kde se točí velké peníze. Vývojem her se zabývá spousta studií po celém světě, včetně České republiky. Nejslavnější herní vývojáři jsou považovány za celebrity ve svém oboru – jména jako John Carmack, Will Wright, Peter Molyneux nebo Ken Levine jsou známá milionům lidí z celého světa. Naproti tomu v technologicky nejvyspělejších státech, jako je Jižní Korea, jsou celebritami i „profesionální hráči“, jejichž zápasy vysílají místní televize v živém přenosu.
1.1
Co je to hra
Existuje několik definic toho, co je to hra. Podle IGDA1 je hra činnost s danými pravidly, která obsahuje konflikt nebo výzvu[13]. Počítačová hra je hra odehrávající se v digitálním prostředí. Proto se počítačovým hrám přezdívá videohry. Pokud budu dále zmiňovat hry, budu myslet ty počítačové, ačkoliv většina věcí platí i pro ty klasické (stolní, karetní a podobně). Většina her má cíle a úkoly, které musí hráč splnit či pokořit osvojením si a použitím herních prvků. Variabilita úkolů a herních prvků závisí na typu hry a cílové skupině hráčů. Jednodušší casual hry pro příležitostné hráče mají obecně minimum herních prvků a herní úkoly se u nich často opakují. Důvodem je, že hráči casual her u nich obecně tráví menší časové úseky, během kterých očekávají zábavu v co nejkratším čase – proto je snaha u 1
International Game Developers Association je nezisková organizace, sdružující více než 10 000 herních vývojářů z celého světa [http://www.igda.org/]
1
2
KAPITOLA 1. ÚVOD
těchto her minimalizovat dobu učení se a co nejdříve hráče přenést do fáze zdokonalování se. Naopak třeba u strategických nebo RPG her trvá dokončení úkolu i několik hodin, během kterých si musí hráč osvojovat nové herní prvky. Hry mívají jasně daný začátek a konec. Výjimku tvoří například on-linové hry, které mají daný konec jen zřídka a hráči je mohou hrát libovolně dlouho. I u nich ale hráči musí plnit dílčí ohraničené úkoly. Další výjimkou mohou být logické a arkádové hry, kde se hráč snaží hrát co nejdéle, aby tak dosáhl nejvyššího počtu bodů. Videohry se snaží hráči zprostředkovat nějaký zážitek. Používají k tomu multimediální obsah – obraz a zvuk. Drtivá většina her obsahuje příběh – některé jsou na vyprávění příběhu přímo založené. Uznávaná britská akademie BAFTA, která každoročně uděluje ceny nejlepším filmům, od roku 2003 vybírá i nejlepší hry za daný rok a uděluje jim ceny za nejlepší příběh, umělecké zpracování, zážitek z hraní a podobně[6]. Každá komerční hra musí před uvedením na trh projít kontrolou odborné komise, která hře udělí rating. Rating má za úkol sdělit potenciálnímu zákazníkovi pro koho je hra určena (věková kategorie), zda obsahuje násilné, sexuální či gamblerské motivy, a podobně[7]. Ve státech jako je USA, Austrálie nebo Německo je podobný rating vyžadován – bez něj se nesmí hra prodávat v obchodech. Existuje několik ratingových systémů – například severoamerický ESRB nebo evropský PEGI. Česká legislativa zatím nijak prodej her podle ratingů neupravuje, ale Asociace herního průmyslu České a Slovenské republiky o přijetí evropského ratingu PEGI v Česku usiluje[8].
1.2
Jak hry dělíme
Hry dělíme do několika herních žánrů. Hranice mezi nimi často bývá tenká – hry mohou kombinovat vlastnosti a prvky několika různých žánrů. Největší rozmach vývoje nastal v 80. a 90. letech 20. století, kdy vznikla jejich drtivá většina. Obecně se dá říci, že od té doby moc nových druhů her nepřibylo. V poslední době vzrůstá obliba rodinných a casual her[10].
1.2.1
Logické hry
Nejde u nich v prvé řadě o vyprávění příběhu a málokdy obsahují násilí. Od hráče je vyžadováno logické uvažování, často v kombinaci s rychlými reflexy. Těmto hrám se daří na méně výkonných zařízeních, jako jsou mobilní telefony, handheldy. Spoustu logických her jde spustit přímo v prostředí webového browseru. Díky své jednoduchosti (ve smyslu komplexnosti, ne obtížnosti) a přístupnosti dokáží zabavit i nehráče. Typickými zástupci jsou: Tetris, Puzzle Quest, Zuma.
1.2.2
Adventury
Adventury bývaly jednu dobu nejvíce oblíbeným druhem her, dnes jsou mírně na ústupu. Hráč prochází herní svět, komunikuje s jeho obyvateli a sbírá různé předměty, které musí správně kombinovat pro další postup a odhalování příběhu. Adventury bývají klidné a nenásilné a často obsahují logické prvky. Příklady: Siberia, Polda, Heavy Rain.
1.2. JAK HRY DĚLÍME
1.2.3
3
Akční hry
Nejrozšířenější a nejprodávanější jsou akční hry. Od hráče vyžadují odvahu, rychlé reflexy a za odměnu nabízí rychlou zábavu, intenzivní vtažení do herního světa a často precizní technické zpracování. V akčních hrách se nezřídka vyskytuje explicitní násilí, krev a prvky nepřátelství. Dělí se na hry z pohledu první osoby a hry z pohledu třetí osoby. Příklady: Call of Duty, Mafia, Halo.
1.2.4
Strategické hry
Poměrně starý žánr, který má nejlepší léta za sebou. Důležitým prvkem je strategické a taktické uvažování. Hraní většinou vypadá tak, že hráč buduje stavby a produkuje herní jednotky, se kterými útočí na nepřítele. Cílem je nepřítele porazit a obsadit jeho území. Existují i varianty bez boje – hráč staví virtuální město a s ostatními soupeří ekonomicky a politicky. Některé strategické hry se dokonce používají ve školách při výuce historie (pro názornou ukázku slavných historických bitev). Příklady: série Command & Conquer, Total War, Anno.
1.2.5
RPG (hry na hrdiny)
Obdoba klasických deskových her převedených na monitory počítačů. Hráč prožívá různá dobrodružství, během kterých si soustavně vylepšuje svou virtuální postavičku. Moderní odnoží jsou tzv. MMORPG – RPG hry pro více hráčů, kteří spolu hrají přes internet.
1.2.6
Rodinné a casual hry
Rodinné a casual hry zažívají nebývalý boom. Výrobci herních konzolí zjistili, jak obrovský potenciál mají příležitostní hráči a proto se jim snaží vyhovět – na všech moderních konzolích a handheldech se dají hrát hry pro sváteční hráče. Nejlépe se ale tomuto žánru daří na osobních počítačích – technicky neznalí lidé nemusí kupovat drahý a složitý hardware a mohou hrát přímo v jejich webovém browseru. Tyto hry typicky nebývají příliš rozsáhlé ani složité – základním požadavkem je, aby hráč mohl začít hrát ihned bez přílišného studování herních technik. Málokdy mají 3D grafické zpracování nebo náročné grafické efekty. Jejich rozmach souvisí s rozšiřování sociálních sítí – desítky milionů uživatelů lidí si na síti facebook oblíbilo hry jako FarmVille nebo MafiaWars, za jejichž hraní nemusí uživatel nic platit. Zisk generují z reklam a tzv. mikrotransakcí, kdy uživatelé nakupují drobné herní předměty a vylepšení pro svou virtuální postavičku za reálné peníze.
1.2.7
Sportovní hry
Dnes má videoherní ztvárnění téměř každý sport. Nejúspěšnější jsou ztvárnění fotbalu, hokeje a basketballu. Sportovní hry mohou pro ovládání využívat nestandartní periferie a ovladače, včetně prvků pohybového ovládání. Příklady: série NHL, Fifa, Pro Evolution Soccer.
4
KAPITOLA 1. ÚVOD
Obrázek 1.1: Plošinová hra Trine (Zdroj obrázku: http://trine-thegame.com)
1.2.8
Plošinové hry
Plošinové hry jsou kombinací žánrů akční hry z pohledu třetí osoby, adventury a logické hry. Z každého žánru si berou určitou část. Hráč vidí avatara zboku a ovládá ho ve dvou dimenzích. Důležitým prvkem je skákání a překonávání překážek. Pro postup hrou může být vyžadováno likvidování nepřátel, řešení logických hádanek nebo sbírání předmětů – to záleží na konkrétní hře. Většinou u nich nejde o realistické zpracování. Ukázku plošinové hry najdeme na obrázku 1.1. Příklady: Mario, Braid, Trine.
1.3
Kdo hry vytváří a jak
Proces vytváření her je rok od roku náročnější a nákladnější. Na počátku herního průmyslu byly hry vytvářeny několikačlennými týmy s minimálním rozpočtem. Dnešní vývoj největších her probíhá ve velkých herních studiích po několik let a stojí desítky až stovky milionů dolarů. Konkrétní rozpočty jednotlivých her vydavatelé neradi sdělují veřejnosti. Na hře se podílí obrovská spousta lidí s různorodými profesemi – od programátorů, game designérů a level designérů, přes 3D grafiky a animátory, až po zvukaře, hudební skladatele a scénáristy. Důležitou částí vývoje je testování kvality – tzv. QA (Quality assurance). Testování probíhá po celou dobu vývoje. Vývoj hry se může nacházet ve dvou fázích: • Preprodukce • Produkce
1.3. KDO HRY VYTVÁŘÍ A JAK
5
Během preprodukce se plánuje, jak bude vývoj vypadat, co přesně se bude vytvářet a zda to má smysl. V preprodukci vydavatel analyzuje trh a rozhodne se, zda bude po titulu dostatečná poptávka, tak aby se zaplatily náklady na vývoj a hra generovala zisk. Designéři a další členové týmu připraví podklady pro další vývoj – základ takzvaného Design dokumentu. Design dokument má za úkol kompletně shrnout veškeré aspekty hry – od popisu herních mechanik, přes artworky a náčrty postav a objektů, rozhovory postav až po rozdělení práce a časový harmonogram. Výsledná hra se může od tohoto dokumentu odlišovat – pokud se například navržené mechaniky neosvědčí v implementovaných prototypech nebo pokud vývojáři nestíhají vše vypracovat do stanovené doby vydání. Přesto má ale design dokument velký význam – bez něj by nemuseli všichni členové týmu sdílet stejnou vizi. Velkým přínosem je pro členy, kteří se do procesu tvorby zapojí až v jeho průběhu. Design dokument (nebo jeho část) také může tvořit šablonu pro budoucí projekty (osvědčené nebo neimplementované mechaniky se převezmou do dalších her a podobně). Během produkce studio implementuje herní mechaniky s pomocí herního enginu. V této fázi se vývoje zúčastňují všichni členové studia. Vytvářejí se skripty, 3D modely, textury, zvuky a podobně. Dále se vytvářejí prototypy, na kterých se testují určité herní prvky. Hra během produkce prochází několika stádii ukončenými tzv. milestony. Posledním milestonem je distribuce zákazníkům. Většina her je vyvíjena i po začátku prodeje – opravují se dříve neobjevené chyby a připravuje se dodatečný herní obsah, leckdy placený (tzv. DLC – Downloadable Content).
6
KAPITOLA 1. ÚVOD
Kapitola 2
Nástroje pro tvorbu her a cíl BP Velice důležité pro vývoj her jsou nástroje, se kterými členové týmu pracují a pomocí kterých hru tvoří. Do těchto nástrojů se počítají zejména herní enginy a editory. To, zda zakoupit hotové nástroje, nebo strávit čas s vývojem vlastních, si musí zodpovědět vývojový tým ještě před začátkem vývoje. Vždy záleží na dostupném rozpočtu a času, který s vývojem hodlají strávit.
2.1
Herní engine
Herní engine je knihovna usnadňující vývoj počítačových her. Engine obsahuje základní nezbytné funkce, společné pro více her. Výsledné hry pak na těchto funkcích staví a rozšiřují je o konkrétní herní mechaniky a specializované funkce, šité na míru danému hernímu žánru. Dobrý engine by také měl oprošťovat od závislosti na konkrétní platformě a řešit optimalizace a výkon. Se vzrůstajícími nároky na paralelizaci se v poslední době od moderního enginu očekává automatická distribuce výpočtů mezi více jader procesoru. Komerčně poskytované enginy jsou navrženy tak, aby s jejich pomocí šel vytvořit jakýkoliv typ hry. Vždy jsou ale primárně optimalizovány pro jeden žánr – například dnes nejrozšířenější engine Unreal byl použit pro výrobu her snad všech možných žánrů, základ má ale v akčních střílečkách. Engine poskytuje rozhraní pro ovládání scény, animací, fyziky, umělé inteligence, nastavení vykreslování nebo zpracování uživatelského vstupu. Měl by poskytovat rozhraní pro skriptování pomocí skriptovacího jazyka. Některé využívají již hotových skriptovacích jazyků a některé přicházejí s jazykem vlastním, navrženým přímo pro typ hry, na který je engine primárně zaměřen. Nejaktuálnější enginy vždy tvoří technologickou špičku a posouvají hranice počítačové grafiky. Technologie od společností Id Software nebo Unreal se díky své hardwarové náročnosti a novátorství používají jako benchmarky pro testování nového hardwaru. Každá generace enginu idTech přináší něco nového do realtimové počítačové grafiky – například třetí generace (idTech 3), použitá v počítačové hře Quake III, vyžadovala jako jedna z prvních grafický akcelerátor a přinesla na svou dobu revoluční zpracování stínů a světelných efektů. 7
8
2.2
KAPITOLA 2. NÁSTROJE PRO TVORBU HER A CÍL BP
Editor
Nároky na editor v posledních letech strmě vzrostly. Před několika lety k enginu existovalo několik samostatných úzce zaměřených nástrojů – například pro nastavení obalových těles modelů, pro nastavení textur a materiálu a podobně. Vývoj ale směřuje k integraci veškerých nástrojů do jednoho programu – editoru. Od moderního špičkového editoru se očekává, že bude fungovat na principu WISIWYG – What you see is what you get. Vývojář vidí v editoru hru naprosto stejně, jako bude vypadat na obrazovce u hráče. Tato vlastnost editorů se stává důležitou díky narůstajícímu množství grafických efektů, náročnosti renderované scény a celkovému důrazu na vizuální zpracování. Editor se v základu podobá klasickým modelovacím nástrojům jako je Autodesk Maya nebo 3D Studio – hlavní část zabírá náhled scény, kterou uživatel editoru upravuje. Dále bývá možnost procházet assety (herní modely a textury), upravovat materiály a shadery nebo psát skripty. Ukázku rozhraní editoru UnrealEd nalezneme na obrázku 2.1.
Obrázek 2.1: Rozhraní editoru UnrealEd (Zdroj obrázku: http://unreal.com) Nejvyspělejší editory poskytují i možnost upravovat cutscény podobně jako animace v softwaru pro střih videa – animátor nastavuje pro klíčové snímky transformace objektů nebo animace a okamžitě si může přehrávat vzniklé sekvence. Díky tomu, že vše probíhá v jednom programu, ušetří vývojáři spoustu času a navíc nemusí řešit podporu formátů, do kterých se sekvence ukládají – editor je ukládá přímo do formátu, který podporuje hra. S tím souvisí (dnes už běžný) požadavek na okamžité spuštění hry v editoru. Pokud vývojář navrhuje level nebo píše skript a nemá možnost hru okamžitě uvnitř editoru spustit, ztrácí spoustu času ukládáním práce v editoru a spouštěním hry. Spouštění hry přímo v editoru také přináší možnost lépe ladit skripty, za běhu hru pozastavovat a upravovat a podobně. Dále najdeme v moderních editorech náznaky vizuálního programování – pomocí tzv. flowchartů vývojář definuje chování herních objektů (typicky umělé inteligence), animova-
2.3. OPEN SOURCE A LOW-COST ALTERNATIVY
9
ných sekvencí nebo propojení materiálů a efektů. Tyto funkce umožňují na hře pracovat i neprogramátorům (designérům či grafikům). Přestože výkon osobních počítačů nezadržitelně stoupá, stále nedokáží počítat vše v reálném čase. Spousta věcí souvisejících s osvětlováním a stínováním se počítá tzv. offline. Příkladem je předpočítání osvětlení pro velké části scény a jeho uložení do tzv. lightmapy. Velice často se podobné věci počítají právě v editoru. Grafik v editoru nastaví pozici světel a řekne editoru, aby osvětlení spočítal a uložil. Hra poté za běhu spočítané osvětlení načte a použije a ušetřený procesorový čas může využít na něco jiného. U editoru se předpokládá, že v něm budou tvořit i technicky méně zdatní lidé – grafici nebo level designéři. Proto by jeho uživatelské rozhraní mělo být co nejintuitivnější a nejjednodušší. Editor tvoří nadstavbu hotového enginu – využívá jeho funkce a přidává nástroje pro vytváření a testování herního obsahu. Hranice mezi enginem a editorem velice často splývá – pokud společnost nabízí vlastní engine k licencování, téměř vždy je součástí i editor. Editory také mohou být distribuovány společně s prodávanou hrou. Cílem je vybízet hráče k vytváření vlastního obsahu – tzv. modů. Kolem takto podporovaných her pak může vzniknout komunita zapálených moderů, kteří ve volném čase tvoří nový herní obsah pro ostatní hráče.
2.3
Open source a low-cost alternativy
Existuje celá řada volně dostupných knihoven usnadňujících vývoj počítačových her. Avšak ne každá se dá nazvat přímo herním enginem. Open-source knihovny jako Ogre nebo Irrlicht se primárně snaží usnadňovat vykreslování 3D scény a volitelně nabízejí propojení s dalšími pomocnými knihovnami - například pro načítání uživatelského vstupu nebo počítání fyziky. Nízká nebo nulová cena je vykoupena často nedostatečnou dokumentací a chybějící oficiální podporou. Také málokdy poskytují výkonné a široce použitelné editory. Výhodou je díky dostupnosti zdrojových kódů možnost si knihovnu dodatečně rozšířit podle vlastních představ. Některé původně proprietární bývají postupem času uvolněny pod volnou licencí – pro jejich použití pak může hrát rozsáhlejší dokumentace. • Box2D1 – Volně dostupná knihovna primárně usnadňující počítání fyzikální simulace ve dvou dimenzích. Tuto knihovnu si zvolilo již několik vývojářů a použilo ji pro tvorbu menších komerčních her jako je Crayon Physics Deluxe nebo Rolando. Knihovna se dá použít na různých platformách – na osobních počítačích, konzoli Nintendo Wii, handheldu Nintendo DS či mobilních telefonech s operačními systémy Android a iOS. • Crystal Space2 – Projekt Crystal Space se skládá ze dvou částí – knihovny pro vykreslování realtime 3D grafiky a pomocných systémů pro vývoj interaktivních aplikací, zejména her. Zahrnuje podporu pro skriptování pomocí jazyka Python, fyzikální simulaci, správu událostí (eventů), zpracování uživatelského vstupu, umělou inteligenci nebo path finding. 1 2
Box2D – oficiální stránky [http://www.box2d.org/] Crystal Space – oficiální stránky [http://www.crystalspace3d.org/
10
KAPITOLA 2. NÁSTROJE PRO TVORBU HER A CÍL BP
• IdTech 3 – V roce 2005 byl pod licencí GNU General Public Licence uvolněn slavný a ve své době revoluční engine idTech třetí generace. Engine podoruje dynamické stíny, simulaci mlhy a odrazů zrcadel. 3D modely načítá z editory široce podporovaného formátu MD3. V roce 2011 má být podobně uvolněna i čtvrtá generace enginu, použitá například v počítačové hře Doom 3. • Torque3 – Cenově dostupný výkonný moderní engine použitý v celé řadě komerčních her. Po zakoupení licence jsou poskytnuty kompletní zdrojové kódy a rozsáhlá dokumentace. Existuje několik verzí: pro 2D hry nebo 3D hry na osobní počítače a pro 2D hry na mobilní zařízení společnosti Apple. Součástí je i editor herního světa, skriptů a materiálů.
Obrázek 2.2: Game Maker je editor pro vytváření jednoduchých her (Zdroj obrázku: http://en.wikipedia.org)
• Game Maker – Proprietární editor, který je hojně používán pro vývoj freeware her. Umožňuje skriptování ve vlastním skriptovacím jazyku GML (Game Maker Language). Základní osekaná verze editoru je k dispozici zdarma, rozšířenější stojí velice přívětivých 25 dolarů. Z ceny je patrné, že editor míří na začínající vývojáře menších her. Game Maker je rozšířený i mezi českými vývojáři freeware her. Rozhraní Game Makeru si můžete prohlédnout na obrázku 2.2. 3
Torque – oficiální stránky [http://www.garagegames.com/products/torque-3d]
2.4. TECHNOLOGICKÁ ŠPIČKA
2.4
11
Technologická špička
Výběr mezi profesionálními enginy je o dost pestřejší. Každé větší herní studio má svůj vlastní a mnohé jej licencují dalším studiím. U některých se dokonce vývoj a prodej enginu stal jejich primární činností. Pro začínající vývojáře a malá herní studia jsou tyto technologie finančně nedostupné – ceny licencí za použití v komerční hrách se pohybují až v řádech stovek tisíců dolarů. Na druhou stranu ale tyto knihovny poskytují naprostou většinu potřebných funkcí, osvědčených použitím v už existujících komerčních hrách s miliony prodanými kusy. Představují současnou technologickou špičku a nesrovnatelně převyšují drtivou většinu zdarma dostupných řešení. Určitou nevýhodou může být mírná vizuální podobnost her postavených na stejných enginech – každý si řeší stínování a nasvícení scény po svém (a používá pro to jiné optimalizace); osvětlení a stínování velice ovlivňuje tzv. grafický feeling. • Unity – Moderní engine se vzrůstající oblibou u vývojářů. Vsází primárně na poskytované Unity Development Environment – robustní editor pro vytváření herního obsahu. Chlubí se možností souběžného spuštění a editování hry. Součástí je modul do webových prohlížečů, s jehož pomocí lze vytvořenou hru spustit přímo v prohlížeči.
Obrázek 2.3: Editor Hammer od společnosti Valve staví na enginu Source (Zdroj obrázku: http://www.thefullwiki.org/Valve_Hammer_Editor)
• Source + Hammer – Engine Source vyvinula společnost Valve pro svou hru Half-life 2. Od té doby ho použila celé řada dalších her. I přes poměrné stáří (hra Half-Life
12
KAPITOLA 2. NÁSTROJE PRO TVORBU HER A CÍL BP
byla vydána v roce 2004) se stále drží na špici a společnost Valve ho neustále vylepšuje – přidána byla podpora pro výpočetně náročné osvětlování HDR, lepší obličejové animace a vícevláknové výpočty. Součástí je velice oblíbený editor Hammer. Kolem Source enginu se utvořila silná základna lidí, kteří ve volném čase tvoří vlastní herní obsah ve formě modů. Valve dokonce umožňuje autorům takto vytvořené mody prodávat ostatním hráčům ve svém internetovém obchodu Steam – pro společnost jde o výhodnou nabídku, protože veškeré mody pro spuštění vyžadují zakoupenou a nainstalovanou hru běžící na Source enginu (například Half-Life 2). Díky velikosti komunity můžeme na internetu nalézt velké množství tutoriálů a návodů, jak s enginem a editorem pracovat. Ukázku z editoru najdete na obrázku 2.3. • Unreal Engine + UnrealEd – Dnes nenajdeme používanější engine než Unreal Engine 3. První generace byla určena pro hru Unreal a díky technologické vyspělosti se začala hojně používat i v dalších hrách. Seznam her, které pohání aktuální třetí generace, by byl velice dlouhý. Cena za licenci oficiálně není veřejně známá, neoficiálně se hovoří o 700 tisíci dolarech[9]. • Cryengine + SandBox – Technologicky nevyspělejší engine, který se snaží o fotorealistickou počítačovou grafiku. Jako jeden z prvních přinesl podporu pro DirectX 10, třetí generace podporuje herní konzole a chlubí se stereoskopickým renderováním bez výrazné ztráty výkonu.
2.5
Cíl práce
Cílem této bakalářské práce je vytvořit specializovaný herní engine (jádro hry), který bude primárně pohánět plošinové hry, a nástroj pro vytváření herního obsahu (herní editor). Editor bude umožňovat upravovat prostředí levelů, definovat herní pravidla, nastavovat fyziku objektů, skriptovat cut scenes sekvence a umělou inteligenci a také hru přímo spouštět a testovat v prostředí. Hra bude samostatně spustitelná i bez editoru. Pro demonstraci implementovaných možností enginu a editoru bude vytvořena jednoduchá ukázková hra s několika úrovněmi. Jádro hry by mělo podporovat: • 3D grafické zobrazení • Načítání 3D modelů, textur a materiálů z externích souborů a jejich správu v paměti • Zpracování uživatelského vstupu • Skriptovatelnou umělou inteligenci, s podporou pro vyhledávání cesty v 3D prostoru (tzv. pathfinding) • Načítání herních levelů z externích souborů • Fyzikální simulaci s nastavitelnými fyzikálními vlastnostmi herních objektů • Animace modelů (včetně skeletální)
2.5. CÍL PRÁCE
13
• Cut scenes – neinteraktivní skriptované sekvence • Skriptovatelný Heads-up display (HUD) pro zobrazení informací o zdraví hráče, munici atd. Editor bude nadstavbou herního jádra. Přidávat bude podporu pro: • Načítání a ukládání herních levelů v editoru • Přidávání, odebírání a rozmisťování herních objektů podobně jako v modelovacích nástrojích • Přímou úpravu herních skriptů – skriptování umělé inteligence, cut scenes, rozhovorů postav, zpracování uživatelského vstupu atd. • Editaci terénu • Spouštění hry uvnitř editoru • Debuggování skriptů s možností pozastavení hry pomocí breakpointů, její krokování, prohlížení obsahu proměnných, zásobníku volání atd. Výsledkem budou 2 samostatné programy. Prvním bude spustitelné herní jádro, které bude načítat a spouštět externě uložený herní obsah – assety, levely a skripty. Druhým pak editor pro vytváření a testování tohoto herního obsahu.
14
KAPITOLA 2. NÁSTROJE PRO TVORBU HER A CÍL BP
Kapitola 3
Analýza technologií Technologií a programovacích jazyků pro vytvoření herního enginu je celá řada, každá má své výhody a nevýhody. Při výběru jsem se řídil mými dosavadními zkušenostmi a snažil jsem se zhodnotit budoucí potenciál každé technologie. Dalším vodítkem bylo, že engine i editor budou sice primárně určeny pro platformu Microsoft Windows, ale zejména engine by mohl být v budoucnu portován na jiné platformy (Linux, MacOS).
3.1
Programovací jazyky
• Java – Objektově orientovaný jazyk s garbage collectorem běžící uvnitř virtual machine. Kód napsaný v Javě se nejdříve překládá do bytecode a ten se kompiluje do strojového kódu až na koncovém zařízení. Důvodem je maximální přenositelnost kódu mezi platformami. Java je tedy multiplatformní, má dobrou dokumentaci a podporu vývojových nástrojů. Pro vývoj herního enginu se ale příliš nehodí kvůli omezené podpoře grafických knihoven (omezeně se dá použít OpenGL), hodila by se spíše pro vývoj editoru. • C#/.NET – C# je programovací jazyk v rámci softwarového frameworku Microsoft .NET. Podobně jako Java se C# překládá do obdoby bytecodu, nazývané CIL (Common Intermediate Language). Přeložený kód se na koncovém zařízení optimalizuje pro danou platformu a kompiluje do strojového kódu. Součástí .NETu je framework XNA, určený pro vývoj počítačových her. Nad použitím právě XNA jsem uvažoval, ale nakonec jsem ho vyřadil z důvodu nekompatibility s dalšími operačními systémy. Existuje sice implementace .NETu na jiné operační systémy nazvaná Mono, jde ale o implementaci neoficiální. Jazyk C# a platformu .NET jsem se rozhodl použít pro vytvoření editoru. Důvodem bylo, že vývoj v C# probíhá obecně jednodušeji a rychleji než v klasickém C++ a také že framework .NET obsahuje z mého pohledu velice dobré API pro tvorbu uživatelského rozhraní. Podle mého názoru je ideální volbou pro vývoj windows-only aplikací, u kterých hraje důležitou roli uživatelské rozhraní a kde je výkon aplikace až druhořadý. • C++ – Jazyk C++ je považován za zatím nepřekonaného lídra, co se týče rychlosti a paměťové náročnosti. Jazyk se ihned kompiluje do strojového kódu, který může nativně 15
16
KAPITOLA 3. ANALÝZA TECHNOLOGIÍ
běžet jen na kompatibilním stroji. Pokud chceme program spouštět na jiné platformě, musíme ho překompilovat. Jazyk umožnuje ovládat hardware na low-level úrovni, nemá vestavěnou automatickou správu paměti a přináší několik mocných, ale nebezpečných nástrojů – ukazatele, přetěžování operátorů, vlastní alokace a uvolňování paměti nebo dynamické knihovny. Vzhledem k tomu, že s ním mám bohaté zkušenosti a že v něm je napsána drtivá většina profesionálních herních enginů, jsem se rozhodl zvolit právě C++.
3.2
Renderovací knihovny
Vykreslování scény může být v enginu řešeno pomocí externí hotové knihovny, nebo přímým použitím některého API (OpenGL nebo DirectX). Vzhledem k tomu, že je tato práce zaměřena na jiné věci než programování rendereru, jsem se rozhodl použít hotovou a osvědčenou knihovnu. Základním požadavkem byla možnost použití knihovny v C++, volná licence, dostačující dokumentace a případná přenositelnost mezi platformami. • Irrlicht – Knihovnu Irrlicht jsem použil při vývoji jedné starší semestrální práce. Irrlicht je podle mého dostačující co se týče rychlosti vykreslování (pro menší hry), obsahuje líbivé moderní grafické efekty a optimalizace a dá se rozšířit celou řadou volně dostupných komponent (například rozšíření pro počítání fyzikální simulace). Bohužel jsem byl mírně zklamán rozsahem oficiální dokumentace a během vývoje zmíněné semestrální práci jsem musel často studovat přímo zdrojové kódy Irrlichtu. Proto jsem chtěl tentokrát zkusit knihovnu jinou. • Ogre – Už z názvu Object-Oriented Graphics Rendering Engine lze usoudit, že je Ogre primárně určeno k vykreslování scény. Nabízí i další dodatečné funkce jako správu paměťově náročných zdrojů v paměti (textury, modely atd.) a jejich načítání ze souborů, načítání uživatelského vstupu nebo vytváření uživatelského rozhraní. Ogre má vlastní formát materiálů, popsaných v textových souborech, včetně možnosti použít shadery. Komunita okolo Ogre vytvořila spoustu pluginů do existujících modelovacích nástrojů pro export modelů a scén. Tuto knihovnu si k vývoji menších komerčních her zvolila už celá řada nezávislých studií. Podporuje vykreslování scény pomocí DirectX, OpenGL a díky tomu je přenositelné na jiné operační systémy a platformy. Ogre jsem se rozhodl použít pro vykreslování a správu scény.
3.3
Fyzikální knihovny
Jedním z vytýčených požadavků na engine je počítání fyzikální simulace. Vzhledem k tomu, že nemám s programováním fyzikálních výpočtů žádné zkušenosti, padlo opět rozhodnutí na použití externí fyzikální knihovny. Požadavkem byl jazyk C++, volná licence a přenositelnost. • ODE – S fyzikální knihovnou ODE mám lehké dřívější zkušenosti a bohužel ne úplně příjemné. Přestože je uvolněna pod open-source licencí, tak ji používá celá řada vysokorozpočtových her. Díky open-source původu má jen minimální dokumentaci. ODE je
3.4. SKRIPTOVÁNÍ
17
napsáno hlavně v jazyce C a využívá minimum výhod novějšího C++. Použití ODE jsem si nechal v záloze pro případ, že bych nenašel vhodnější alternativu. • Bullet – Fyzikální knihovna uvolněná pod svobodnou licencí. Je používaná i pro počítání fyzikálních efektů ve filmech nebo renderovacích nástrojích. Velikou výhodou je optimalizace pro běh na více-jádrových systémech, část výpočtů umí přesunout i na grafickou kartu. Použít se dá na osobních počítačích s různými operačními systémy, herních konzolích a mobilních telefonech. Bullet jsem zvolil jako hlavní knihovnu pro počítání fyzikální simulace a zjišťování kolizí. • PhysX – Proprietární komerční řešení fyzikálních výpočtů. Pro použití ve freeware hrách je PhysX zdarma, pro použití v komerčních hrách musí být zakoupena licence. V porovnání s knihovnami ODE nebo Bullet je PhysX zdaleka nejkomplexnější a nejlépe zdokumentovanou. Nad jejím použitím jsem uvažoval, odradila mě uzavřenost a licencování u komerčních her.
3.4
Skriptování
Skriptování herní logiky, umělé inteligence a cutscén vyžaduje rychlý a flexibilní skriptovací jazyk. Skriptovacích jazyků existuje celá řada a nedá se s klidným srdcem rozhodnout, který je lepší a který je horší. Každý má svá specifika a hodí se na něco jiného. Velice mě lákalo zkusit navrhnout a implementovat jazyk vlastní. Bohužel v této oblasti nemám dostatečné zkušenosti a s vývojem bych strávil nezanedbatelné množství času. Proto padlo rozhodnutí použít nějaké hotové a osvědčené řešení. • Python – Python je díky své minimalističnosti a jednoduchosti velice používaný pro tvorbu rozšíření do existujících softwarů nebo pro vytváření webových stránek. Jako skriptovací jazyk se ve hrách příliš nepoužívá. • Lua – Pokud vývojáři neimplementují vlastní skriptovací jazyk, pak většinou volí právě jazyk Lua. Je znám pro svou paměťovou nenáročnost a jednoduchost napojení na existující aplikace. Lua má velice specifický objektový model, který nemusí vyhovovat každému. Dá se říci, že je tento jazyk dnes považován za standard v oblasti skriptování her. Proto jsem se ho rozhodl použít. • Vlastní – Pokud bych vytvářel vlastní skriptovací jazyk, musel bych navrhnout jeho syntaxi a implementovat virtual machine, který by ho vykonával. V implementování virtual machine se skrývá spousta záludností spojených s optimalizováním paměťové náročnosti a rychlosti vykonávání skriptů. Toto téma by vydalo na samostatnou bakalářskou práci.
18
KAPITOLA 3. ANALÝZA TECHNOLOGIÍ
Kapitola 4
Návrh aplikace Při vytváření aplikace bych rád použil moderní postupy, jako je událostmi řízené programování, rozdělení kódu do samostatných systémů, využití task-based a vícevláknového programování a přesunutí herní logiky do skriptů. Při návrhu budu brát v potaz možnost budoucího rozšiřování enginu. Engine by měl důsledně oddělovat vykreslování, fyzikální simulaci a herní logiku pro snazší udržitelnost a budoucí rozšiřitelnost kódu. Každý systém si bude držet svou sadu dat a nezasahovat přímo do ostatních. Engine by měl nabízet úpravu a nastavení většiny svých možností z externě uložených uživatelských skriptů. Skriptování by mělo hrát důležitou úlohu, proto bude jádro hodně obecné a nebude neomezovat rozmanitost výsledných her (v rámci zvoleného žánru plošinových her). Data herních úrovní by měla být načítána z externích souborů.
4.1
Událostmi řízená aplikace
Komunikace a předávání dat mezi objekty tvoří základní stavební kámen současných programovacích jazyků. Nejzákladnějším typem komunikace se rozumí volání metod, předáváním dat pak jejich parametry. Taková komunikace vyžaduje, aby její iniciátor věděl o každém, s kým chce komunikovat (a mohl zavolat jeho metodu). Na druhé straně pokud příjemce dat je už nadále nechce odebírat, musí přímo upozornit odesílatele. Je jasné, že taková komunikace vyžaduje, aby oba účastníci měli k dispozici přístup k tomu druhému. Vzniká přímá závislosti mezi objekty. Většinou se přílišné přímé závislosti v aplikaci snažíme vyhnout, třeba použitím komunikace nepřímé – vyvoláním události. Událost (event) je zpráva upozorňující na skutečnost, že proběhla nějaká akce nebo změna. Zároveň nese data s akcí související. Každý kdo se o ní dozví, může zareagovat. Vyvoláním události odesílatel říká: Dávám na vědomí, že jsem provedl tuto akci, a nezajímá mě, jak zareagujete. Prozatím teoretický popis událostí doplním zjednodušeným příkladem: Dejme tomu, že herní objekt Hráč má nějaké Zdraví. Pokaždé, když se mu hladina Zdraví zvýší nebo sníží, odešle událost HealthChangedEvent. Tu zachytí objekt starající se o vykreslování zdraví hráče (HUD) a upraví ukazatel Zdraví na novou hodnotu. Dále se o události dozví systém pro zpracování zvuku, a pokud je zdraví pod kritickou hodnotou, začne přehrávat zvuk 19
20
KAPITOLA 4. NÁVRH APLIKACE
class EventManager
«interface» Event «static» + InternalEventGroup: EventGroup + GameLogicEventGroup: EventGroup + ScriptEventGroup: EventGroup + ExternalEventGroup: EventGroup + + + + + + + +
getTimestamp() : unsigned long isOfGroup(EventGroup &) : bool isScriptOnly() : bool initScriptData() : bool buildScriptData() : void destroyScriptData() : void getType() : const EventType& getGroup() : const EventGroup&
«interface» EventManager + + + + + + + +
startReceiving(EventReceiver*, EventType&) stopReceiving(EventReceiver*, EventType&) startReceiving(ScriptCallback&, EventType&) stopReceiving(ScriptCallback&, EventType&) queueEvent(Event*) : void abortEvents(EventType&, bool) : void abortEvents(EventGroup&) : void abortAllEvents() : void
«static» + getInstance() : EventManager* + setInstance(EventManager*) : void + deleteInstance() : void
ScriptCallback + + + +
handleEvent(Event&) : bool saveScriptData() : void loadScriptData() : bool clearScriptData() : void
«interface» EventReceiver +
handleEvent(Event&) : bool
Obrázek 4.1: Třídy související s událostmi
tlukotu srdce. Bez použití událostí by Hráč musel buď přímo upozornit HUD a systém pro zpracování zvuku, anebo by se příjemci museli Hráče neustále dotazovat na hladinu Zdraví. Způsobů implementace událostí nalezneme několik. V základu se dělí na lokální a globální – buď se o jejich odebírání zaregistrujeme přímo u odesílatele a pak odebíráme jen zprávy od něj, anebo se zaregistrujeme u centrálního (globálního) mezičlánku a odebíráme zprávy od všech. Tento engine bude stavět na globálně šířených událostech použitím centrálního správce eventů (EventManager) – viz obrázek 4.1. Pokud bude nějaká třída potřebovat použít lokální události, poskytne pro to rozhraní ve stylu návrhového vzoru Observer. Pokud chceme odebírat událost, musíme EventManageru sdělit o jaký typ jde. Z toho důvodu je potřeba události nějak rozlišovat. Nejjednodušší rozdělením může být výčtový typ enum – každý nový typ eventu si do globálního výčtového typu přidá nový záznam. Obrovskou nevýhodou takového řešení je omezení typů na dobu kompilace – poté nejde přidávat nové typy událostí. Navíc by takový enum časem nabobtnal do těžko udržovatelného výsledku. Proto padlo rozhodnutí použít dynamické typy eventů pomocí hešovaného textu – každý typ (EventType) bude hešová hodnota textového názvu (např. pro event změny pozice objektu LevelObject_PositionChangedEvent). Dále se vyplatí dělit události do skupin – například skupina interních událostí enginu nebo skupina eventů herní logiky. EventManager tyto skupiny opět rozlišuje pomocí heše (EventGroup). Proč zavádět skupiny? Pokud například skončí jedna herní úroveň (level), potřebujeme invalidovat veškeré události spojené s herní logikou, protože už nadále nemají smysl – řekneme EventManageru aby zrušil
21
4.2. SYSTÉMY
všechny eventy skupiny GameLogicEventGroup (abortEvents). Distribuce eventu neprobíhá ihned po jeho zařazení, ale hromadně v každém jednom kroku aplikace. Během distribuce EventManager projde frontu a každou událost rozešle všem odběratelům. Odběratelé mohou být buď v rámci aplikace (EventReceiver), nebo v rámci skriptu (ScriptCallback). Pokud odběratel vrátí hodnotu true, považujeme událost za vyřízenou a přestáváme ji propagovat. Pokud vrátí false, pokračujeme v rozesílání. Při vyvolávání eventu netušíme, kdo, kdy a v jakém pořadí se o něm dozví. Proto vyžaduje událostmi řízené programování mírně odlišný způsob uvažování a přístup k návrhu ostatních tříd. Velikou výhodou je odstranění závislostí a snadná rozšiřitelnost celé aplikace – zejména v kombinaci se skriptováním. Nové typy událostí můžeme vytvářet i ve skriptech a defaultně jsou zařazeny do skupiny ScriptEventGroup. Takto vytvořené eventy jsou obsluhovány pouze ze skriptů. Při návrhu systému eventů jsem se částečně inspiroval v knize Game Coding Complete[14].
4.2
Systémy
Při návrhu každé aplikace je důležité ji dobře rozvrhnout do několika částí pro oddělení nesouvisející logiky. Každá taková část by měla být samostatná, snadno rozšiřitelná a vyměnitelná. Větší funkční celky se vyplatí načítat ze samostatných knihoven – právě kvůli snadné obměně, ale i pro snížení doby kompilace hlavního programu a třeba i pro rozdělení práce mezi více lidí (každý pracuje na jiné části).
class Systems
«interface» Game::GameloopListener
SystemManager «virtual» + addSystem(string, System*) + loadSystem(string, string) + hasSystem(string) : bool + getSystem(string) : System* + removeAllSystems() + removeSystem(string) «static» + getInstance() : SystemManager* + setInstance(SystemManager*) + deleteInstance()
«virtual» + onInit(Game*) + onUpdate(Game*, long) + onDestroy(Game*)
«interface» System +
getSystemName() : string
Obrázek 4.2: SystemManager sdružuje veškeré systémy Sumarizací těchto požadavků vzniká něco jako systém – samostatný funkční celek, který se v aplikaci vyskytuje právě jedenkrát. Příkladem může být fyzikální systém pro počítání fyzikálních výpočtů nebo animační systém zaštiťující vše okolo animací.
22
KAPITOLA 4. NÁVRH APLIKACE
To jak systémy zorganizujeme, ovlivní celkovou práci a manipulaci s nimi. Jednou z možností je, co systém, to statická instance v nějaké třídě (návrhový vzor Singleton) – jeho instance je pak přístupná odkudkoliv z aplikace. Nevýhodou je psát pro každý systém kód pro přístup. Navíc takové řešení ztěžuje přidávání nových systémů do již hotové aplikace. Vhodnější variantou je manažer spravující veškeré systémy (SystemManager). Ten umožní jejich přidávání i za běhu. Systémy budou přístupné všude tam, kde bude přístupná instance manažeru. V našem enginu bude tento manažer statickou instancí, Singletonem. Díky „všudypřítomnosti“ mohou vznikat skryté závislosti (jeden systém využívá jiný systém), proto musíme být při jejich používání obezřetní. Návrh SystemManageru nalezneme na obrázku 4.2. Systémy indexujeme podle názvu a drtivou většinu vytvoříme na začátku běhu aplikace – ať už přímým vytvořením a přidáním instance (addsystem), nebo načtením z dynamické knihovny (loadSystem). Každý povinně implementuje rozhraní GameloopListener a odebírá tak kritické interní události: Inicializace (Init), Aktualizace (Update) a Ukončení (Destroy). SystemManager každý nově přidaný automaticky zaregistruje pro odebírání těchto událostí.
4.3
Herní smyčka a třída Game
Základem každého herního enginu je variace na herní smyčku – neustále se dokola opakující sled událostí: 1. Zpracuj vstup od uživatele (proces user input) 2. Aplikuj změny (update) 3. Vykresli scénu (render scene) Konkrétní implementace se odvíjí od ostatních vlastností a požadavků – v každém kroku nemusí být nutné ani výhodné provádět všechny tři kroky. Například zpracovat vstup a herní logiku stačí typicky 20krát až 30krát za sekundu, zatímco počet vykreslených snímků musí pro zachování plynulosti obrazu být větší – 30 snímků je minimum, optimálně by jich mělo být 60. Vzhledem ke zvolenému událostmi řízenému typu této aplikace, můžeme všechny tři kroky sloučit do jednoho – Distribuuj eventy (viz obrázek 4.3). Během distribuce se zpracuje nahromaděná fronta EventManageru a upozorní všichni odběratelé GameloopListener na událost Update – mezi ně patří i systém pro zpracování uživatelského vstupu nebo systém pro vykreslování scény (každý zaregistrovaný systém povinně odebírá událost Update). V jednom kroku herní smyčky bude skutečně explicitně docházet jen a pouze k distribuci událostí. Počet těchto updatů za jednu sekundu bude pevně stanoven. Původně třetí krok – vykreslení scény – obstará systém Graphics. Ten se při obdržení události Update sám rozhodne, zda dojde k vykreslení snímku. Vše záleží na zvoleném nastavení a implementaci, bližší popis nalezneme v kapitole 4.4. Renderer totiž může běžet na samostatném vlákně a v tom případě si to, kdy dojde k vykreslení scény, řeší sám. Přidělení vlastního vlákna přináší výhodu v oddělení počtu aktualizací herní logiky a počtu vykreslení scény. Pokud zvolíme nevláknovou implementaci, bude vykresleno tolik snímků za
23
4.4. GRAPHICS A THREADEDGRAPHICS
act Game loop Game start
Initialise
«iterative»
[while running] Distribute events
Destroy
Game end
Obrázek 4.3: Herní smyčka událostmi řízeného enginu
sekundu, kolik bude updatů celé aplikace. Na první pohled nevýhodné řešení může mít smysl při použití enginu na zařízení s nízkým výkonem – například chytrém mobilním telefonu nebo tabletu. Při navrhování třídy Game (viz obrázek B.1) byla snaha o maximální kompaktnost. Třída sice řeší obecně inicializaci celé hry a tikot herní smyčky, ale jen minimum dalších funkcí. Vzhledem k tomu, že přístup k ní nepotřebuje téměř žádná další třída, nebyl důvod volit návrhový vzor Singleton, přestože bude s největší pravděpodobností existovat vždy jen jedna instance.
4.4
Graphics a ThreadedGraphics
Systém Graphics zaštiťuje veškeré operace spojené s vykreslováním scény a manipulaci s grafickými daty. Staví na knihovně Ogre a poskytuje její rozhraní ostatním. Při návrhu byla snaha o odstínění práce s objekty Ogre a zároveň připravení třídy na možné vícevláknové zpracování. Zvolil jsem tzv. task-based programming – rozdělení do samostatně spustitelných úloh (tasks). Pokud bude potřeba manipulovat s Ogre objekty nad rámec poskytovaného rozhraní, zapouzdříme operace do úlohy (GraphicsTask) a vložíme do fronty pro zpracování úloh (TaskManager). GraphicsTask zpřístupňuje další operace pro práci s Ogre. O tom, kdy dojde ke zpracování úloh, si rozhoduje samo Graphics. Pokud vytváříme objekty scény, použijeme rozhraní GraphicsData – Graphics opět rozhodne, kdy zavolá operace pro vytvoření
24
KAPITOLA 4. NÁVRH APLIKACE
(createData), synchronizaci (synchronizeData) a smazání dat (destroyData) objektu – data mohou být v rámci optimalizace vykreslování scény kdykoliv smazána či znovuvytvořena, proto se na ně po vytvoření nikde nespoléháme (tyto metody mohou být volány z jiného vlákna). Všechny herní objekty implementují rozhraní GraphicsData. act ThreadedGraphics Initialise
«parallel»
[another thread] [while rendering]
«iterative» Process Tasks
Synchronize data
Render frame
Destroy
Obrázek 4.4: Životní cyklus vícevláknového rendereru ThreadedGraphics Životní cyklus Graphics nalezneme na obrázku 4.4. Po inicializaci startuje smyčka, kde v každém kroku probíhá posloupnost: (1) Zpracuj nahromaděné úlohy (Process tasks), (2) Synchronizuj data (Synchronize data), (3) Vykresli snímek (Render frame). Pokud jde o nevláknovou variantu Graphics, tento krok proběhne vždy při události Update třídy Game. V opačném případě – při použití ThreadedGraphics – tato smyčka běží na vlastním vlákně a tedy nezávisle. Výhodou ThreadedGraphics je lepší rozložení zátěže a možnost kontrolovat hladinu snímkové frekvence (setTargetFps). U nevláknové je maximální počet snímků roven počtu aktualizací hry za sekundu.
4.5
Task Manager
Task based programming využijeme nejen u Graphics, ale i dalších částech aplikace. Úlohy postupně zařazujeme do fronty zpracování (addTask). Pokud je chceme zpracovat, přepneme na další frontu (switchTaskQueue), abychom během zpracovávání mohli přidávat úlohy nové, a ze staré fronty postupně vytahujeme úlohy (processOneTask), dokud není
25
4.6. STATE A STATECONTROLLER
prázdná (hasTask). Třída nabízí rozhraní pro odebírání událostí spojených se zpracováním úloh (TaskManager::Listener) – umožňuje úlohy filtrovat nebo jim předávat dodatečná data. Operace TaskManageru vidíme na obrázku B.3. Základní verze správce úloh (SimpleTaskManager) není thread-safe. Pro více vláknové zpracování se hodí jiná varianta, zamykající data před ostatními (SafeTaskManager). Volba správce ovlivní výkon – každé zamknutí nás stojí drahocenný procesorový čas. S rozčleněním kódu do úloh se pojí podobný problém jako u událostmi řízeného programování – při psaní kódu tasku netušíme, kdy dojde k jeho vykonání. Také netušíme přesné pořadí, v jakém úlohy proběhnou (pokud je přidává několik vláken zároveň). Na začátku návrhu aplikace jsem počítal s masivnějším použitím úloh a jejich automatickou paralelizací mezi více vláken. Takové řešení by znamenalo lepší rozložení výpočtů mezi více procesorových jader. Jenže zmíněné vlastnosti mě od toho nakonec odradily. Větší použití a paralelizace tasků bude cílem dalšího researche a vývoje. Prozatím využívá vláknové zpracování tasků jen Graphics, nevláknové pak volitelně další části aplikace.
4.6
State a StateController
Typická hra začíná úvodní obrazovkou, následuje menu, kde hráč zvolí, zda chce začít novou hru, nebo načíst uložený postup či změnit nastavení. Pokud zvolí novou hru, hra přehraje úvodní filmeček (intro) a načte a spustí první level. V každém tomto okamžiku se hra nachází v určitém stavu (State). Každý stav zobrazuje něco jiného a jinak reaguje na vstup od hráče. Proto engine obsahuje systém StateManager, kterým rozlišuje, ve kterém stavu se nachází. Všechny stavy mají jméno, pomocí kterého je identifikujeme. Na začátku vytvoříme a zaregistrujeme všechny možné stavy (addState) a poté mezi nimi přepínáme (changeState). Stavy se dají přidávat a odebírat i v průběhu hry, například pomocí skriptů. class States «interface» StateManager «virtual» + addState(State*) + removeState(State*) + hasState(string) : bool + changeState(string) : State* + getActiveState() : State* + deactivateActiveState() «Event» + StateChangedEvent()
«interface» StateController
«interface» State «virtual» + getName() : string + isActive() : bool + setController(StateController*) + getController() : StateController* + removeController() + setInput(InputSystem*) + activate() + deactivate()
«virtual» + keyPressed(KeyEvent) : bool + keyHeld(KeyEvent) : bool + keyReleased(KeyEvent) : bool + mouseMoved(MouseEvent) : bool + mousePressed(MouseEvent) : bool + mouseReleased(MouseEvent) : bool + activated() + deactivated()
Obrázek 4.5: Manažer stavů a zpracování vstupu Každý stav může mít právě jeden ovladač (StateController), který odebírá události od systému pro zpracování uživatelského vstupu (InputSystem). O zařazení ovladače do InputSystemu se automaticky postará State, který obdrží připravenou instanci InputSystemu v okamžiku, kdy je aktivován manažerem. Propojení tříd vidíme na obrázku 4.5.
26
KAPITOLA 4. NÁVRH APLIKACE
4.7
Načítání a ukládání dat
class BinarySerializedFile
BinaryData «virtual» + serialize(char *, unsigned int) + deserialize(char*, unsigned int) + getSerializationStart() : char* + getSerializationLength() : unsigned int + addData(char *, unsigned int) + endDataTier() + nextDataTier() + resetData() + getNextData(unsigned int&) : const char* + hasNextData() : bool + hasSpace(unsigned int) : bool
«interface» Serializable «virtual» + beforeSerialization() + afterSerialization() + beforeDeserialization() + afterDeserialization() + getDynamicDataSize() : unsigned int + createEmptyData() : BinaryData* + createEmptyData(unsigned int) : BinaryData* + serialize(BinaryData*) + deserialize(BinaryData*)
«interface» BinarySerializedFile «virtual» + addData(Serializable*) + saveFile(std::string) + loadFile(std::string) + hasNextData() : bool + getNextData() : Serializable*
Obrázek 4.6: Třídy pro ukládání a načítání dat Ukládání dat a jejich zpětné načítání je nezbytnou součástí enginu. Rozlišujeme mezi používáním běžnému člověku nečitelného binárního souboru a srozumitelnější textové varianty – například formát xml. Každý způsob má své výhody a nevýhody a každý se hodí k jinému účelu a pro jiná data. Vzhledem k realtime povaze enginu se z hlediska výkonu vyplatí pro většinu dat používat binární formát – je datově úspornější a dá se zpracovávat rychleji (odpadá nutnost parsování). Proto bude engine poskytovat rozhraní pro ukládání a načítání objektů do binárního bloku dat – tzv. binární serializaci. Takto serializovaná data můžeme uložit do souboru či odeslat po síti. Každá třída, která implementuje rozhraní Serializable (viz obrázek 4.6), může být uložena do souboru BinarySerializedFile.
4.8
Struktura levelu
Herní úrovně (Levely) rozčleňují hru do menších úseků. Obsahují veškeré objekty virtuálního světa, ve kterém se hráč pohybuje, a definují pravidla, podle kterých děj hry probíhá. Prapředkem všech herních objektů je třída LevelObject (viz obrázek B.4), která dědí od PhysicsObject a je tak fyzikálním objektem. To je důležitý předpoklad – všechny herní objekty podporují fyziku. Třída Model představuje 3D grafický objekt s podporou animací (startAnimation). Typickým příkladem Modelu jsou objekty tvořící pozadí a prostředí levelu. Character je rozšířením Modelu, které už tvoří ovladatelnou postavu – „herce“ (Actor). Character může být ovládán hráčem, skriptem, nebo umělou inteligencí. Pro spínání skriptovaných událostí použijeme Trigger a ScriptedTrigger, které jsou typicky spínány kolizí s nějakým jiným objektem. Každý LevelObject implementuje rozhraní Serializable a může být tedy uložen do souboru nebo odeslán po síti a zpětně načten. Levely budou primárně ukládány Editorem během jejich vytváření a testování.
27
4.9. EDITOR
Každá úroveň může mít přiřazený svůj vlastní skript, který je vykonán při spuštění levelu. Skriptem můžeme přidávat další objekty, ale hlavně definovat herní pravidla – co hráč smí a co nesmí.
4.9
Editor
Editor staví na API enginu a přináší minimum další funkčnosti. V zásadě jde jen o uživatelské rozhraní, proto nebude z technologického ani návrhového hlediska (návrhu tříd) příliš popisován. Bude využívat u platformy .NET standardního rozdělení do formulářů a komponent. Událostmi řízené programování je zabudováno přímo v jazyce C#. Pro podporu historie uživatelem provedených akcí a také kvůli vícevláknovému přístupu bude veškerá komunikace editor/engine probíhat pomocí operací zapouzdřených do tříd implementujících rozhraní EditorOperation. Každá operace bude reprezentována instancí této třídy. Uživatel díky tomu bude moc využívat výhod historie akcí a vracet zpět provedené operace. Souhrn levelů, skriptů, assetů a nastavení upravovaných v editoru bude ukládán do jednoho projektového souboru. Uživatel načte uložený projekt a může pokračovat v práci. Editor bude podporovat debuggování skriptů, včetně možnosti krokování kódu, procházení zásobníku volání a zobrazování obsahu proměnných ve skriptu. Veškerý kód pro debuggování bude součástí enginu, editor k němu jen poskytuje rozhraní.
Obrázek 4.7: Rozvržení rozhraní editoru Návrh rozvržení rozhraní editoru je na obrázku 4.7. V horní části okna (1) bude standardní horizontální lišta menu pro načítání a ukládání projektu, levelů, skriptů a podobně a pod ním nástrojové lišty pro ovládání projektu, spouštění hry a používání debuggeru. Okno bude vertikálně rozděleno na dvě části (šířku si uživatel bude moci přizpůsobit), každá část bude obsahovat další záložky. Vlevo (2) budou neměnné záložky pro úpravu nastavení projektu, levelu, vybraného objektu a zvoleného nástroje pro úpravu. Napravo (3) budou
28
KAPITOLA 4. NÁVRH APLIKACE
dynamické záložky pro úpravu herního obsahu – například záložka s načteným levelem, textovými skripty a podobně. V okně záložek mohou být další nástrojové lišty, jejichž obsah bude záležet na typu záložky (úpravu jakého typu obsahu nabízí) a zvoleném nástroji pro úpravu (například nástroj pro manipulaci s objektem levelu a podobně). Ve spodní části (4) mohou být ještě další záložky s aditivním obsahem – například výpis konzole, debugové informace (výpis proměnných, zásobník volání a podobně). Úprava levelu bude probíhat vizuálně – uživatel uvidí náhled levelu stejně jako ve hře. Bude moci vybírat herní objekty kliknutím myši a transformvat je manipulátory – ve středu objektu bude zobrazen klasický tříosý manipulátor podobně jako ve většině editorů pro úpravu 3D objektů. Uživatel bude moci posouvat, přibližovat a oddalovat kameru.
Kapitola 5
Popis implementace V této kapitole popíšu jen některé zajímavé části implementace. Veškeré třídy implementované v C++ se nacházejí v namespace RD, psané v C++/CLI v RD_NET a ty v C# pak v RDEditor.
5.1
Reference counting
Protože je většina objektů sdílena mezi jádrem, editorem a skripty, těžko se definuje kdy je vhodné nepoužívané objekty mazat. Většinou je nemůžeme mazat ručně v C++, protože mohou být používány v ostatních částech s automatickou správou paměti. Ani nemůžeme mazání přenechat garbage collectoru .NETu nebo jazyku Lua, protože ten by nám objekt smazal, přestože bychom ho ještě používali v jiné části. Řešením je u takto sdílených objektů udržovat počet jejich referencí – kolikrát je na objekt odkazováno. Pokud počet referencí klesne na nulu, můžeme objekt smazat. Pro počítání referencí je použito tzv. chytrých pointerů (smart pointers). Smart pointer je instance třídy s přetíženými operátory, které automaticky zvyšují a snižují počet referencí na spravovaný objekt. Knihovna Ogre používá smart pointerů knihovny boost – jde o třídu boost::shared_ptr. Tato třída dovoluje spravovat jakákoliv dynamicky alokovaná data a počet referencí udržuje nezávisle na objektu a počet si předává mezi instancemi smart pointeru. Takové chování by pro většinu normálních situací dostačovalo, má však jednu nevýhodu. Spravovaný objekt nemá žádnou vazbu na počet referencí, takže pokud jeho holý ukazatel předáme nové instanci smart pointeru, dojde pravděpodobně k vícenásobnému uvolnění paměti. Kvůli podpoře skriptování potřebujeme operovat i s holými ukazateli, kde by podobné chování způsobovalo potíže. Pointery boost::shared_ptr jsou použity jen pro počítání referencí instancí třídy Event, která není přímo ve skriptech ukládána (do skriptů ukládáme jen data eventu, ne ukazatel na jeho instanci). Pro ostatní objekty jsem vytvořil vlastní specializovanější smart pointery – nazývám je reference (třída „ref“). Data o počtu referencí jsou uložena přímo ve spravovaném objektu a třída ref s nimi jen manipuluje. Podobají se referencím v jazyku C# nebo Java. Třída ref nijak neomezuje práci s objektem. Umožňuje například referenci předávat jako parametr 29
30
KAPITOLA 5. POPIS IMPLEMENTACE
metod a bere v úvahu substituční princip – pokud volaná metoda očekává rodičovský typ, objekt je automaticky (ale bezpečně) přetypován i se zachováním počtu referencí. Výpis 1 1 2 3
ref
p l a y e r = l e v e l −>g e t P l a y e r ( ) ; / / ziskani reference p l a y e r −>s e t W e i g h t ( 5 0 . 0 F ) ; / / manipulace s objektem c a m e r a −>f o l l o w ( p l a y e r ) ; / / metoda follow ocekava nadtyp ref
Každý objekt, který chce být referencován, musí dědit od třídy Referenceable. Ukázku práce s referencí si můžeme prohlédnout ve Výpisu 1.
5.2
Výjimky
Jazyk C++ umožňuje vyhodit jako výjimku naprosto jakýkoliv datový typ. Tato mírná anarchie má za následek absenci informace o místě původu výjimky. Bez připojeného debuggeru neznáme při zachycení výjimky místo, odkud byla vyvolána. S použitím maker se tomuto dá zabránit a do výjimky zanést jméno souboru a číslo řádku, kde byla vytvořena. Ve Výpisu 2 nalezneme rodičovskou třídu všech výjimek enginu a ukázku definice konkrétní výjimky a jejího použití. Pokud je definováno makro RD_USE_MACRO_EXCEPTIONS, bude vyvolaná výjimka obsahovat název souboru a číslo řádku, kde byla vytvořena. Veškeré výjimky enginu dědí od RD::Exception a nabízejí příslušné makro. Výjimky související se skriptováním navíc ještě obsahují informace název skriptu a číslo řádku skriptu, kde k chybě došlo, a výpis zásobníku volání (skriptu). Výpis 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/ / rodicovska trida vech vyjimek enginu class RD : : E x c e p t i o n
{
public : std : : string std : : string
e x c e p t i o n ; / / nazev vyjimky f i l e n a m e ; / / název souboru
l o n g l i n e ; / / íslo ádku s t d : : s t r i n g m e s s a g e ; / / zprava o chybe
public : E x c e p t i o n ( c o n s t E x c e p t i o n& e ) Exception ( std : : string message
; , std : : string exception = " Exception " , s t d : : s t r i n g f i l e n a m e = " " , l o n g l i n e = −1) ; virtual std std std
~ E x c e p t i o n ( ) ; / / virtualni destruktor pro zamezeni memory leaku
: : string getName ( ) ; : : string getMessage ( ) ; : : string getFilename ( ) ;
31
5.3. HERNÍ JÁDRO, SMYČKA
20 21 22 23 24 25 26 27
getLine
long
};
() ;
/ / ukazka specificke vyjimky class RD
{
: : S e r i a l i z a t i o n E x c e p t i o n : public RD : : E x c e p t i o n
public : SerializationException ( std
,
28 29 30 31 32 33 34 35 36 37 38 39 40
: : s t r i n g m e s s a g e , s t d : : s t r i n g f i l e n a m e = " "←-
= −1) : Exception ( message , " SerializationException " , filename , line )
long line
{ }
#i f d e f R D _ U S E _ M A C R O _ E X C E P T I O N S # define SerializationException ( message ) \ SerializationException ( message , __FILE__ , __LINE__ ) #e n d i f }; / / ukazka vyhozeni vyjimky, do ktere je automaticky ulozen nazev souboru a cislo radku throw S e r i a l i z a t i o n E x c e p t i o n ( " Length of s e r i a l i z e d data is i n v a l i d " ) ;
5.3
Herní jádro, smyčka
Herní jádro řeší inicializaci a uvolnění systémů, zanesení definic tříd pro skripty a v neposlední řadě tikot herní smyčky. Herní smyčka je součástí metody Game::run() a je implementována jako pseudo-nekonečný cyklus s omezeným počtem updatů za jednu sekundu. Na každý update tak vychází určitý časový úsek – pokud update trvá kratší dobu než tento úsek, je na zbytek času jádro (respektive vykonávající vlákno) uspáno; pokud trvá déle, zkrátí maximální stanovaný čas dalšího updatu. Při dlouhých updatech se tak automaticky sníží jejich počet. Maximální počet je standardně stanoven na 25 updatů za 1 sekundu, což je pro aktualizaci herní logiky naprosto dostačující. Během jedné aktualizace jsou distribuovány všechny nahromaděné události a aktualizovány všechny systémy. Systém během této aktualizace obdrží údaj o časovém úseku, který uplynul od minulé aktualizace – to je výhodné například pro aktualizaci fyzikálního modelu nebo animací.
5.4
Graphics
Jak už bylo řečeno v návrhové části 4.4, systém Graphics (viz obrázek B.2) zaštiťuje veškeré operace spojené s vykreslováním scény a manipulaci s grafickými daty. Staví na knihovně Ogre a poskytuje její rozhraní ostatním. Práce s Ogre vypadá následovně: srdce knihovny se nazývá Ogre::Root a obsahuje a spravuje veškeré subsystémy. Po inicializaci Ogre::Root, zvolíme Ogre::RenderSystem (například DirectX) a okno Ogre::RenderWindow s jedním nebo
32
KAPITOLA 5. POPIS IMPLEMENTACE
několika Ogre::Viewport. Dále vytvoříme Ogre::SceneManager pro vykreslovanou scénu. Vykreslitelné objekty přidáváme do Ogre::SceneManager jako potomky Ogre::SceneNode. Graphics řeší vytváření většiny Ogre objektů, v externě definovaných grafických datech (GraphicsData) většinou vytváříme až objekty scény – Ogre::SceneNode, Ogre::Entity a podobně. Interface RD:: Graphics má 2 defaultní implementace: RD::OgreGraphics a RD::ThreadedGraphics, která přesouvá běh OgreGraphics na vlastní vlákno. Třída Game podle předaného inicializačního nastavení RD::Settings vybere jednu z těchto implementací. Kvůli vícevláknovosti Graphics používá TaskManager pro veškeré operace vyžádané zvnějšku. Samostatně běžící vlákno si pak úlohy vytahuje a zpracovává. Vlastní úlohy můžeme do toho manažeru zařazovat i externě – stačí rozšířit třídu GraphicsTask, která poskytuje přístup i k protected metodám Graphics. Díky tomu lze libovolně upravovat chování a nastavení rendereru. Vláknová implementace spouští pseudo-nekonečnou smyčku velice podobnou herní smyčce 5.3 – opět jde o pseudo-nekonečnou smyčku s maximálním počtem updatů za jednu sekundu – počet vykreslených snímků za vteřinu (FPS – frames per second). Tento počet můžeme externě nastavovat (setTargetFps) a pokud nastavíme hodnotu na nulu, renderer nebude nijak FPS limitovat – takové chování znamená maximální možný počet FPS, což ale může značně zatěžovat hardware. U nevláknové verze toto nastavení počtu FPS nemá význam a renderer ho nijak nebere v potaz.
5.5
Zamykání (thread-safeness)
Všechny vícevláknové aplikace musejí brát v potaz paralelní přístup ke společným datům a nějak se s tímto faktem vypořádat. Při ignorování toho problému dochází k nepředvídatelným a obtížně zachytitelným chybám – klasickým příkladem je pokud první vlákno čte data, které druhé vlákno v tu samou chvíli upravuje. První vlákno tak může načíst jen zčásti upravená data, což je velkým problémem, pokud například data reprezentují ukazatel na adresu v paměti – výsledkem je nesmyslná adresa a možný pád aplikace. I při použití zámků musíme být obezřetní. Pokud po zpracování dat neodemkneme zámek, kterým jsme zamezovali manipulaci s daty ostatním, můžeme způsobit trvalé uzamčení dat. K této situaci může například dojít, pokud během zpracování dat dojde k vyhození výjimky. Další komplikaci způsobí, pokud si dvě vlákna zamknou data navzájem – první vlákno čeká na dokončení operace druhého a druhé vlákno čeká na dokončení operace prvního. Výsledkem je zamrznutí obou vláken. Pro eliminaci těchto problémů jsem použil mutexy a zámky z knihovny boost – konkrétně boost::recursive_mutex a boost::unique_lock. Tato kombinace umožňuje vícenásobné zamykání dat stejným vláknem a díky RAII1 přístupu eliminuje nutnost ručního odemykání – každý zámek je reprezentován objektem na zásobníku a je tak platný jen v rámci dané části kódu (scope). Při jeho odstranění ze zásobníku dojde automaticky k odemčení mutexu. Všechny části kódu využívající zámky nabízejí makra, kterými jde zamykání zakázat a povolit. Tyto makra se nacházejí v hlavičkovém souboru „app_pch.h“ – pro zakázání zámků 1 Resource Acquisition Is Initialization je technika zaručující, že dojde uvolnění alokovaných prostředů za všech okolností. Technika využívá faktu, že jediná část kódu, která je automaticky volaná během výjimky či ukončení volání funkce, je destruktor.
33
5.6. BINÁRNÍ SERIALIZACE OBJEKTŮ A JEJÍ ZÁLUDNOSTI
určité části stačí zakázat příslušné makro a překompilovat aplikaci. Vliv mají následující makra: • RD_GENERATOR_THREADSAFE – zámky u generátoru terénu • RD_EVENTMGR_THREADSAFE – zámky u EventManageru • RD_GRAPHICS_THREADSAFE – zámky u OgreGraphics Dále je ve stejném souboru makro RD_DEBUG_LOCKS, jehož definice způsobí výpis každého uzamknutí mutexu, což může být nápomocné při hledání chyb souvisejících se zámky.
5.6
Binární serializace objektů a její záludnosti
Základní myšlenkou binární serializace implementované v jazyce C a C++ je přímé kopírování surových dat objektu. Tuto skutečnost musíme při psaní serializované třídy brát v potaz – nemůžeme například používat pointery na jiné objekty, čímž se ochudíme o možnost ukládat dynamicky alokovaná data a tedy i data o předem (myšleno v době kompilace) neznámé velikosti. Při navrhování rozhraní rozhraní jsem se proto snažil tyto nedostatky eliminovat a umožnit ukládání jakýchkoliv dat o dynamické velikosti a zanořování objektů do sebe. Výsledkem je třída BinaryData, pomocí které předáváme data mezi serializovanou třídou implementující Serializable a mezi souborem dat BinarySerialiedFile (viz obrázek 4.6). Pokud potřebujeme ukládat data o dynamické velikosti, naalokujeme instanci BinaryData dostatečně velikou a třída je sama přiloží za konec standardní délky objektu. Při deserializaci je postupně vytahujeme. Data s dynamickou velikostí mohou být například textové řetězce, obrázky nebo například jiná už serializovaná BinaryData. To nám umožní pomyslně zanořovat objekty do sebe. Ve Výpisu 3 nalezneme ukázku serializace třídy LevelObject s využitím LevelObjectData, které dědí od třídy BinaryData a přenáší data mezi serializovaným blokem dat (např. souborem na disku) a jejich majitelem (instance třídy LevelObject). Výpis 3 1 2 3 4 5 6 7 8 9 10 11
# p r a g m a BINARY_DATA_START struct LevelObjectData : public
PhysicsObjectData
{
/ / definice vsech dat o predem zname velikosti bool
boundingBoxVisible
;
LevelObjectData ( unsigned int length = sizeof ( LevelObjectData ) unsigned int dataStart = sizeof ( LevelObjectData ) ) : PhysicsObjectData ( length , dataStart )
{ }
,
34
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
KAPITOLA 5. POPIS IMPLEMENTACE
}; # pragma
void
BINARY_DATA_END
LevelObject
{
: : s e r i a l i z e ( RD : : B i n a r y D a t a ∗ d ) const
/ / ulozeni dat rodicovske tridy t h i s −>P h y s i c s O b j e c t : : s e r i a l i z e ( d ) LevelObjectData ∗ data
;
= ( LevelObjectData ∗) ( d ) ;
/ / ulozeni dat o predem zname velikosti d a t a −> b o u n d i n g B o x V i s i b l e = g e t S h o w B o u n d i n g B o x / / ulození dat o dynamicke velikosti d a t a −>a d d D a t a ( o b j e c t I D . c _ s t r ( )
}
, objectID . length ( ) ) ;
/ / ukoncení bloku dat teto tridy (mohou nasledovat data dcerine tridy) d a t a −>e n d D a t a T i e r ( ) ;
void
{
LevelObject
: : d e s e r i a l i z e ( RD : : B i n a r y D a t a ∗ d )
/ / nacteni dat rodicovske tridy t h i s −>P h y s i c s O b j e c t : : d e s e r i a l i z e ( d a t a ) LevelObjectData
;
∗ data = ( LevelObjectData ∗) ( d ) ;
/ / nacteni dat o predem zname velikosti s e t S h o w B o u n d i n g B o x ( d a t a −> b o u n d i n g B o x V i s i b l e ) i f ( d a t a −>h a s N e x t D a t a
{
}
}
() ;
;
() )
/ / nacteni dat o dynamicke velikosti unsigned int length = 0 ; c o n s t c h a r ∗ i d = d a t a −>g e t N e x t D a t a ( l e n g t h ) s e t O b j e c t I D ( s t d : : s t r i n g ( id , l e n g t h ) ) ;
;
/ / presun na dalsi blok dat (mohou nasledovat data dcerine tridy) d a t a −>n e x t D a t a T i e r ( ) ;
S binární serializací se pojí několik záludností. Zaprvé nesmíme ukládat ukazatele na jiné objekty. Tento problém by bylo velice těžké obejít (třída by například musela poskytovat nějaké rozhraní pro mapování objektů), ale protože jsem se v celé aplikaci obešel bez ukládání pointerů, nerozebíral jsem dále tento problém a ukládání ukazatelů tak BinaryData nepodporuje. Zadruhé by ukládaná třída neměla mít žádné virtuální metody, protože ukazatel na virtuální tabulku by po deserializaci s největší pravděpodobností měl nesmyslnou hodnotu. BinaryData tento problém řeší tak, že ukládá a načítá data až od části s reálnými daty a
5.7. FYZIKA BULLET
35
pointeru na virtuální tabulku si nevšímá. Zatřetí může nastat problém při přenosu dat mezi různými platformami (stačí i použít jiný kompilátor). Problémem je, že každý kompilátor si optimalizuje reálné velikosti tříd a ukládá do nich výplňová data (kvůli zarovnání a související optimalizaci). Toto chování se dá potlačit pragma direktivou – proto musí být definice dceřiných tříd od BinaryData obalena direktivami #pragma BINARY_DATA_START a #pragma BINARY_DATA_END. Začtvrté je s různými platformami problém jejich odlišného ukládání dat – Big a Little Endian. V současnosti BinaryData tento problém nijak neřeší, data budou validní pouze na stroji s architekturou, pro kterou byl kód zkompilován. Pokud by do budoucna vyvstal požadavek používat uložená data na obou platformách zároveň, musela by se do třídy BinaryData přidat podpora pro automatické převedení data mezi těmito systémy ukládání. Zapáté může každý kompilátor používat jinou velikost vestavených datových typů – většinou záleží pro jakou platformu je kód kompilován (x86, x64 atd.). Doporučuji u všech skalárních datových typů používat tzv. bit field, kterým kompilátoru řekneme, jakou mají mít data velikost. Bit field nefunguje pro float/double/long double, které by ale měly mít stejnou délku i reprezentaci všude tam, kde je dodrženo standardu IEEE 754. Při serializaci méně obvyklých datových typů (například wchar_t) musíme být obezřetní a zjistit si, jak jsou kompilátorem reprezentovány.
5.7
Fyzika Bullet
Implementaci fyziky Bullet nemá cenu moc popisovat – engine využívá standardní rozhraní knihovny a nepřidává moc funkčnosti navíc. Veškeré fyzikální nastavení je uloženo v objektu RD::PhysicsObject, od kterého dědí všechny herní objekty levelu. Mezivrstva RD::PhysicsSystem si vytáhne tyto informace a vytvoří podle nich příslušné objekty Bulletu pro popis tzv. rigid body (simulace reálného chování tuhých těles). Fyzikální knihovna se také používá pro přesné vyhledávání objektů v prostoru. Do budoucna by mohlo být výhodné přesunout počítání fyziky na vlastní vlákno a využít v Bulletu vestavěné podpory výpočetní síly grafických akcelerátorů. Dále by bylo užitečné lépe začlenit skriptovatelnost přímo rigid body objektů a událostí při jejich kolizích.
5.8
Umělá inteligence
Základem umělé inteligence implementované v enginu jsou tzv. agenti (RD::Agent). Svého agenta může mít každý actor (RD::Actor, RD::Character). Všichni agenti mohou být zavedeni do správce umělé inteligence (RD::AISystem), který se postará o jejich automatickou aktualizaci v každém kroku hry. Pokud má Character přiřazeného agenta, postará se sám o jeho zavedení do správce. Agent má definované stavy (RD::AgentState), ve kterých se může nacházet, a cíle (RD::AgentGoal), kterých se snaží dosáhnout. Každý cíl se může skládat z několika dalších podcílů a ty zase z dalších podcílů a tak dále. Cíle i stavy mají většinu metod virtuálních a podporují callbacky ze skriptů (viz kapitola 5.9.3), takže můžeme snadno definovat a upravit jejich chování – u obou jde zejména o metody activated, deactivated a step.
36
KAPITOLA 5. POPIS IMPLEMENTACE
Cíle mohou být velice jednoduché, nebo naopak velice komplexní. Také můžeme z jednoduchých cílů poskládat jeden komplexní. Příklady možných cílů: dojít na nějaké místo, najít nepřítele (tedy hráče), vystřelit na nepřítele a podobně. Cíle se nemusejí použít jen k vytvoření iluze umělé inteligence, ale třeba i k obyčejnému naskriptování nějaké posloupnosti akcí – využití tak najdou při vytváření cutscén. Při implementaci umělé inteligence jsem čerpal z knih Programming Game AI by Example[11] a Game Coding Complete[14].
5.8.1
Vyhledávání cesty v prostoru, A* algoritmus
Velice důležitou součástí reálně se tvářící umělé inteligence je, aby se dokázala orientovat a pohybovat v prostoru. Vyhledávání a sledování optimální cesty v prostoru je v našem případě velice zkomplikováno zaměřením enginu na plošinové hry a využitím fyzikální simulace. Pokud by například byl engine zaměřen na 2D strategické hry, nemusel by při hledání cesty brát v úvahu gravitaci nebo způsob pohybu postaviček. V tomto případě se však musí vypořádat s tím, že postavička (pokud má věrně simulovat pohyb člověka) může nejen chodit, ale i skákat a padat, a že se nemusí umět dostat na všechny bodu prostoru (nedoskočí tak daleko, nepřejde příliš velikou překážku, nevyjde příliš příkrý svah a podobně). Aby mohla umělá inteligence efektivně hledat cestu v prostoru, potřebuje ho rozdělit na menší úseky a ty propojit do grafu. U spousty typů her jde prostor dělit automaticky (například podle mřížky), kvůli výše zmíněným problémům by ale bylo dělení složité, proto jsem zvolil manuální definici grafu s tím, že do budoucna se může automatické dělení doimplementovat. Editor umožňuje vytvářet tzv. cesty (RD::Route), po kterých může postava putovat (viz obrázek C.1). Cesty se mohou libovolně větvit a jejich základ jde vygenerovat z vytvořeného terénu (částečná náhrada za chybějící automatické dělení prostoru). Za běhu hry se tak už cesty nevytvářejí, ale načítají hotové a převádějí na prostorový graf (RD::SpatialGraph), ve kterém umělá inteligence vyhledává cestu z bodu A do bodu B. SpatialGraph se skládá z uzlů (RD::SpatialNode) pospojovaných hranami (RD::SpatialArc). Uzel má pozici v prostoru, toleranci (jak moc se od něj může postava vzdálit) a seznam hran, které z něj vedou. Hrany jsou obousměrné a obsahují odkaz přesně na 2 uzly. Hrana umožňuje spočítat svou délku a natočení (úhel), což se hodí pro zjištění, zda postavička hranu dokáže přejít. Správce umělé inteligence obsahuje všechny grafy popisující prostor levelu a poskytuje rozhraní pro vyhledávání cesty - RD::PathFinder. Engine v současnosti obsahuje jedinou implementaci a to je AStarPathFinder, která vyhledává cestu pomocí algoritmu A*. A* je optimálně efektivní algoritmus pro hledání nejkratší cesty v prostoru stavějící na algoritmu Dijkstra a rozšiřující ho o heuristiky pro minimalizaci počtu hledání. Princip algoritmu je následující: Procházíme graf a zpracováváme jeho uzly pomocí prioritní fronty (prvky jsou seřazené podle hodnoty F, viz dále), na začátku fronta obsahuje jen startovní uzel. Postupně z fronty vytahujeme uzly. Pokud je vytažený uzel roven tomu cílovému, našli jsme cestu, v opačném případě zpracujeme všechny jeho sousedy. Pokud už byl sousední uzel zpracován, přeskočíme ho. Pokud nebyl zpracován, spočítáme výhodnost vedení cesty tímto uzlem – algoritmus počítá 3 hodnoty pro každý uzel: • G (goal) – celková „cena“ dosavadní cesty z počátku do aktuálního uzlu
37
5.8. UMĚLÁ INTELIGENCE
• H (heuristic) – odhadovaná „cena“ z aktuálního uzlu do cíle (to, jakým způsobem je tato hodnota odhadována, značně vlivní efektivnost algoritmu; engine ji počítá jako přímou vzdálenost mezi oběma uzly) • F (fitness) – odhadovaná „cena“ cesty z počátku do cíle přes aktuální uzel, tj. součet G+H Pokud vychází uzel výhodněji než doposud nejvýhodnější cesta, řekneme, že cesta povede tímto uzlem a přidáme ho do fronty. Pokud skončí zpracovávání fronty a my jsme nenašli cestu, znamená to, že žádná taková neexistuje. K tomuto může dojít, pokud startovní uzel a cílový uzel nejsou nijak propojeny. Celý algoritmus je ve Výpisu 4. Výpis 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
ref A S t a r P a t h F i n d e r SpatialNode > to )
{
f r o n t A d d ( c r e a t e N o d e ( from while
{
: : f i n d P a t h ( ref <S p a t i a l N o d e > f r o m , ref <←-
, to , N U L L ) ) ; / / zaciname pocatecnim uzlem
( ! frontEmpty () )
N o d e ∗ n o d e = f r o n t P o p ( ) ; / / AStarPathFinder::Node obaluje SpatialNode n o d e −>c l o s e ( ) ; / / oznacime jako zpracovany i f ( n o d e −>g e t S p a t i a l N o d e
{
return
}
( ) == t o )
buildPlan ( node )
SpatialNodeList
; / / mame cestu
; ( )−> a p p e n d N e i g h b o r s ( n e i g h b o r s ) ;
neighbors
n o d e −>g e t S p a t i a l N o d e
for ( S p a t i a l N o d e L i s t : : i t e r a t o r it n e i g h b o r s . e n d ( ) ; i t ++)
{
Node ∗ neighbor if ( n e i g h b o r
{ }
= t r a n s l a t e (∗ it ) ;
== N U L L )
f r o n t A d d ( c r e a t e N o d e ( ∗ it
, to , n o d e ) ) ; / / novy uzel - nejvyhodnjsi cesta
e l s e i f ( n e i g h b o r −>i s C l o s e d
{
continue
}
= n e i g h b o r s . b e g i n ( ) ; i t != ←-
() )
; / / jiz zpracovany uzel
e l s e i f ( n e i g h b o r −>g e t G o a l ( ) >= n o d e −>g e t G o a l getCostToNeighbor ( neighbor ) )
{
n e i g h b o r −>s e t P r e v i o u s ( n o d e ) frontAdd ( neighbor ) ;
( ) + n o d e −>←-
; / / vyhodnejsi nez soucasna cesta
38
34 35 36 37 38 39
KAPITOLA 5. POPIS IMPLEMENTACE
}
}
}
return NULL
}
5.9
; / / zadna cesta neexistuje
Skriptování a jazyk Lua
API pro napojení Lua skriptů do aplikace je kompletně v jazyku C, což znamená, že na jednu stranu by mělo fungovat na všech platformách, na druhou stranu však přináší určité komplikace. Naštěstí je API dobře zdokumentované, při vývoji jsem čerpal z oficiálních dokumentů a volně dostupných knih od autorů jazyka[12][15]. Všechny funkce a datové struktury z Lua API začínají „lua_“. Třídy pro práci s tímto API jsem umístil do namespace „RD::Scripting::Lua“. Reprezentace virtuální stroje se nazývá lua_State, pro snazší práci s ním má engine třídu Lua::State. Laždý state běží samostatně a nezávisle na ostatních. Engine vytváří jeden hlavní a jeden pro level a herní logiku (ten je při každém načtení levelu restartován). Staty mezi sebou nesdílí žádná data, mají jiné sady globálních proměnných, engine do nich ale automaticky zaregistruje stejné definice obalených tříd a pomocných funkcí – většina herních objektů se tak dá ovládat a vytvářet přímo ze skriptů. State umožňuje spouštět Lua skripty uložené v souborech – skript je tak vázán na konkrétní state. Třída Lua::LuaManager inicializuje prostředí Lua a vytváří staty. Umožňuje jejich vytváření, mazání, ale i restartování. Automaticky do nich zanáší zmíněné definice tříd a funkcí a řeší správu chyb a výjimek, které mohou nastat při práci se skriptem – ať už jde o chybnou syntaxi (compile-time error) nebo o chyby během vykonávání skriptu (run-time error). Pro ladění run-time chyb obsahuje debugger – pokud je debugger povolen, předá se zpracování chyby jemu, v opačném případě vyhodí výjimku RuntimeException.
5.9.1
Správa zásobníku
Filosofií luovského API je přenášet veškerá data z aplikace do statů (a naopak) přes pomyslný zásobník. Pokud chceme například do lua_State přidat globální proměnou, uložíme ji na vrchol zásobníku – od té chvíle leží na zásobníku pod indexem -1 (stack top). Dále na zásobník vložíme ve formě řetězce klíč, pod kterým bude tabulka uložena – název proměnné. Tím se index tabulky na zásobníku změnil na -2 (jednu pozici pod vrcholem zásobníku), klíč má index -1. No a konečně statu řekneme, ať položku zásobníku s indexem -2 uloží do globálních proměnných pod klíčem, který najde na indexu -1. Je jasné, že taková práce s indexy je mírně komplikovaná a hlavně náchylná k programátorským chybám – stačí malá chyba při počítání indexů během psaní kódu a je zle. Proto jsem se snažil vytvořit nadstavbu nad standardním API, která by správu indexů a vůbec celého zásobníku řešila sama. Nadstavba se používá v celé aplikaci a automatizuje práci se zásobníkem, což vedlo k výraznému omezení počtu chyb a zjednodušení manipulace se staty. Stále je však dobré pro použití této nadstavby chápat, jak zásobník a indexy fungují.
5.9. SKRIPTOVÁNÍ A JAZYK LUA
39
Třída Lua::State řeší vytváření obalů zásobníku Lua::StateStack – obalů může být více, protože Lua otevírá nový zásobník pro každé volání funkce. Neexistuje žádný jeden globální zásobník – vždy je závislý na tom, co zrovna lua_State provádí. Lua::StateStack v kombinaci se Lua::StackString, Lua::StackInteger a dalšími třídami automaticky spravuje indexy a velice usnadňuje používání zásobníku.
5.9.2
Boxing objectů
Používání objektů a tříd a volání metod z C++ v jazyku Lua není až tak triviální. V rámci rešerše jsem hledal podobná řešení a většinou šlo jen o používání statických C funkcí, málokdy o použití objektů, metod a tříd, včetně podpory dědičnosti. Existují sice hotové knihovny, které toto řeší, žádná se mi ale nezamlouvala. Proto mi zabralo spoustu času vymyslet vhodné rozhraní pro používání objektů enginu ve skriptech. Tato část aplikace se postupem času hodně vyvíjela podle potřeb a s prapůvodním návrhem vytýčeným na počátku vývoje nemá mnoho společného. Pro třídy, které chceme použít i ve skriptech musíme vytvořit další obalové třídy (wrapper) podobně jako jsme to dělali pro použití tříd v jazyce C# (viz kapitola 5.10). Lua nezná objekty, ale tabulky ukládající data pod textovými klíči. Pokud chceme ve skriptu používat něco ve stylu objektů a metod, vytvoříme tabulku, do které uložíme ukazatel na objekt a nastavíme jí jako metatable tabulku obsahující ukazatele na C++ funkce. Metatable je v tomto případě něco ve smyslu definice třídy – pokud se z původní tabulky představující objekt snažíme získat prvek pod nějakým neznámým klíčem, dotáže se Lua na tento prvek metatable. Metatable navrátí funkci (pokud existuje), která (pokud ji správně zavoláme) automaticky obdrží jako první parametr původní tabulku. Ve funkci musíme z tabulky získat ukazatel na objekt, převést parametry metody do C++ dat a zavolat příslušnou metodu objektu. Ve skutečnosti je okolo ještě spousta další práce – musíme spravovat zásobník, hlídat správnost předaných argumentů, ošetřovat možné výjimky volané metody a podobně. Vytvořil jsem rozhraní, pomocí kterého snadno vytvoříme definice obalených metod a tříd (včetně podpory dědičnosti). Dále jsem vytvořil sadu maker, které velice usnadňují získávání ukazatelů na obalené objekty, převod dat, zachytávání výjimek a podobně. Díky tomu je obalování metod a funkcí docela jednoduché. Ukázku obalení jednoduché metody nalezneme ve Výpisu 5 a to, jak by kód vypadal po rozbalení maker ve Výpisu 7. Ukázka obalení mírně komplexnější metody je pak ve Výpisu 8. Výpis 5 1 2 3 4 5 6 7 8
/ / priklad volani teto funkce ve skriptu: / / local bullet = controller:shoot(x = 10, y = 0, z = 0); int Lua_ActorController : : shoot ( lua_State ∗ l )
{
/ / (I) nastaveni poctu parametru RD_LUA_ARGS (1) ; / / (II) typ volani a nazev tridy
40
9 10 11 12 13 14 15 16 17 18 19 20 21 22
KAPITOLA 5. POPIS IMPLEMENTACE
RD_LUA_THISCALL ( ActorController ) / / (III) prevod parametru RD_LUA_ARG_VECTOR3 ( target )
;
;
/ / (IV) zavolani obalene metody/funkce ref b u l l e t = _ _ t h i s −>s h o o t ( t a r g e t ) / / (V) navratova hodnota RD_LUA_RETURN_OBJECT ( bullet )
}
;
;
/ / (VI) signalizuje ukoncení volani funkce RD_LUA_END () ;
Nejprve nastavíme počet parametrů volané funkce (I). Lua podporuje volání funkcí s variabilním počtem paramterů, což ale pro vytváření wrapperu není vhodné, protože obaluje klasické s C++ funkce a metody s pevným počtem parametrů. Pokud počet parametrů nesouhlasí, wrapper vyhodí chybu. Dalším důvodem, proč hlídáme počet parametrů, je získávání tabulky obaleného objektu a hlídání způsobu volání funkce. Lua umožňuje dva způsoby volání funkcí – buď pomocí tečky, což odpovídá volání statické funkce, nebo pomocí dvojtečky, což se podobá volání metody – Lua při takovém volání automaticky nastaví jako první parametr tabulku, ze které funkci voláme. Ve skriptech je k této tabulce přístup pod jménem „self“ (obdoba klasického „this“), přes Lua API ji najdeme naspodu zásobníku (Lua argumenty funkce na zásobník vkládá zleva). Různými způsoby volání se dostáváme k nastavení typu funkce a jménu třídy (II). Jedná se o makra: • RD_LUA_STDCALL – značí volání pomocí tečky, tedy statickou funkci • RD_LUA_THISCALL – značí volání pomocí dvojtečky, tedy metodu (makro získá„__this“ ukazatel na obalený objekt a „__table“ pro manipulaci s tabulkou) • RD_LUA_NEW – značí volání pomocí tečky, je obdobou konstruktoru (makro vytvoří tabulku pro nový objekt) Dále nastavíme typy parametrů a převedeme je do C++ dat (III). Parametry bychom měli získávat v pořadí, v jakém je u funkce očekáváme zleva. Převod usnadňují makra: • RD_LUA_ARG_INTEGER, RD_LUA_ARG_DOUBLE, RD_LUA_ARG_BOOLEAN, RD_LUA_ARG_STRING, RD_LUA_ARG_TABLE - podle názvů je jasné, jaké parametry makra obstarávají • RD_LUA_ARG_OBJECT – značí, že je parametrem obalený objekt – makro získá ukazatel na tento objekt (makro má druhý parametr, který udává jakého typu objekt je)
5.9. SKRIPTOVÁNÍ A JAZYK LUA
41
• RD_LUA_ARG_REF – podobné jako RD_LUA_ARG_OBJECT, jen pracuje referencemi (ne ukazateli) • RD_LUA_ARG_QUATERNION, RD_LUA_ARG_VECTOR2, RD_LUA_ARG_VECTOR3 – převádějí data vektorů a quaternionů Máme převedené parametry, takže zavoláme samotnou funkci, metodu, kontruktor a podobně (IV). Konečně nastavíme návratovou hodnotu (V), wrapper pro to nabízí makra podobně jako u parametrů: • RD_LUA_RETURN_INTEGER, RD_LUA_RETURN_DOUBLE, RD_LUA_RETURN_BOOLEAN, RD_LUA_RETURN_STRING, RD_LUA_RETURN_TABLE, RD_LUA_RETURN_QUATERNION, RD_LUA_RETURN_VECTOR2, RD_LUA_RETURN_VECTOR3 – mají dostatečně výmluvné názvy, jediným parametrem je vždy hodnota daného typu • RD_LUA_RETURN_OBJECT vrací obalený objekt, jediným parametrem je ukazatel nebo referenci (v tomto případě se nerozlišuje chování), pokud je ukazatel nebo reference nulová, vrací hodnotu nil • RD_LUA_RETURN_NIL vrací hodnotu nil (obdoba null v C# nebo NULL v C++) • RD_LUA_RETURN_ERR vrátí chybovou hodnotu (v současné době nepodporuje chybovou zprávu) • RD_LUA_RETURN nevrací žádnou hodnotu (obdoba návratového typu void) Na úplném konci (VI) musí být ještě makro RD_LUA_END, které ošetří a zpracuje možné výjimky, uzavře zásobník a ukončí volání funkce.
5.9.3
Eventy ve skriptech
Událostí můžeme ve skriptech používat dva druhy. První možností jsou globálně šířené, kdy použijeme EventManager podobně jako v klasickém kódu: EventManager.startReceiving(Level.StartedEvent, myLevelCallback); EventManager pak při distribuci eventu Level::StartedEvent volá funkci onLevel_Started tabulky myLevelCallback. Druhým typem eventů jsou tzv. callbacky. Každý objekt, který může být obalen do skriptu, musí dědit od třídy Scriptable, takže obsahuje metody getCallback a setCallback
42
KAPITOLA 5. POPIS IMPLEMENTACE
(souhrnně property Callback). Ve skriptu nastavíme jako callback nějakou tabulku, u které bude daný objekt enginu hledat funkce související s jeho funkčností. Pokud callback tabulka obsahuje hledanou funkci, nahrazuje tím defaultní chování objektu – to je podstatný rozdíl oproti globálně šířeným událostem, které jen mohli stanovit reakci na nějakou událost. Callbacky můžeme přímo upravit chování enginu. Zavedení callbacku je velice jednoduché: myGameState.Controller.Callback = myGameController; Callbacky najdou největší využití při skriptování herní logiky, reakcí na uživatelský vstup, umožňují definování pravidel umělé inteligence.
5.9.4
Debugger a krokování v editoru
Obrázek 5.1: Editor obsahuje debugger pro krokování kódu Lua samotná nic jako debugger nenabízí, její API jen dovoluje při většině situací volat callback funkci (v terminologii jazyku Lua se nazývají hooks), čehož debugger využívá. Debugger umožňuje nastavit skriptům breakpointy, které pozastaví vykonávání kódu a předají řízení debuggeru. Debugger můžeme ovládat buď z kódu, nebo v editoru. Na obrázku 5.1 vidíme editor s pozastaveným skriptem. V horní liště se nachází tlačítka pro ovládání
5.10. MEZIVRSTVA C++/CLI
43
debuggeru (umožnují pozastavený kód krokovat, pouštět nebo úplně zastavit), dále vidíme kód skriptu včetně nastaveného breakpointu a ve spodní části výpis lokálních a globálních proměnných. Během pozastavení můžeme procházet i zásobník volání – výpis lokálních proměnných je vždy poplatný vybranému zanoření.
5.9.5
Ukázka skriptování
Skriptování nabízí ještě další věci, které jsem dosud moc nezmiňoval. Wrapper umožňuje přístup k datům obaleného objektu pomocí skrytého volání getrů a setrů, tak jak to známe z jazyka C# (tzv. properties). Dále si například obalené objekty ukládají uživatelská data ze skriptů. Tato data jsou nepřenosná mezi Lua staty. Ukázku skriptů pro zpracování uživatelského vstupu a nastavení pravidel umělé inteligence najdeme ve Výpisu 9.
5.10
Mezivrstva C++/CLI
Veškeré třídy enginu jsou psány v jazyce C++ a řadí se tedy mezi unmanaged kód – výsledný program běží nativně a bez garbage collectoru. Aby bylo možné používat unmanaged třídy v managed jazyku (C#), bylo nutné vytvořit tzv. wrapper. Zvolil jsem řešení vytvořit wrapper v jazyce C++/CLI, což je jazyk platformy .NET, který umožňuje míchat C++ a C++/CLI kód – v rámci platformy .NET se tento přístup nazývá IJW (It Just Works). Výsledek se zkompiluje do DLL knihovny, která se dá načíst a použít v C# projektu. Wrapper je obalení původních unmanaged tříd novými managed třídami. Veškeré třídy wrapperu jsou součástí namespace RD_NET (pro odlišení od původního namespace RD). Wrapper tedy musí řešit převod objektů jedné platformy do druhé (tomuto procesu se říká Marshalling) a dále by měl zachovávat původní hierarchii tříd a umožňovat tak přetypovávání stejně jako v originálních třídách. Pro sjednocení rozhraní wrapperu jsem vytvořil 2 základní třídy RD_NET::PtrObject a RD_NET::RefObject, od kterých dědí všechny ostatní obalové třídy. PtrObject se používá pro ukládání přímých ukazatelů na instance objektů enginu a RefObject pro ukládání referencí (tedy instancí těch tříd, které dědí od RD::Referenceable). Tím se přenáší reference counting i do editorové části (detaily o počítání referencí naleznete v kapitole 5.1). Obě třídy (PtrObject i RefObject) mají template metodu GetPtr, kterou získáme příslušný ukazatel (nebo referenci). Ukázka implementace obalení jedné metody v jazyce C++/CLI je včetně vysvětlení ve Výpisu 6. Výpis 6 1 2 3 4 5 6 7 8
/ / trida RD_NET::LevelObject je potomkem tridy RD_NET::RefObject void RD_NET
{
}
: : LevelObject : : AddedToLevel ( RD_NET : : Level^ level )
t h i s −>G e t P t r () / / ziskani reference ref −> a d d e d T o L e v e l ( / / zavolani unmanaged metody l e v e l −>G e t P t r () / / predani parameteru
);
44
KAPITOLA 5. POPIS IMPLEMENTACE
Marshalování vestavěných datových typů (int, char atd.) si řeší platforma .NET sama. Marshalování stringů a kolekcí musíme řešit sami – wrapper pro většinu těchto situací obsahuje statické funkce třídy RD_NET::Marshal. Wrapper dále obaluje i události a umožňuje tak jejich odebírání v jazyce C#. Také využívá tzv. properties (automatického volání setrů a getrů) pro zachování konvencí jazyka C#.
5.11
Komunikace Engine / Editor
Editor obstarává herní smyčku enginu sám – má pro to rozhraní RDEditor.GameLoop, konkrétně implementace: nevláknovou SynchronizedGameLoop a ThreadedGameLoop běžící na vlastním vlákně. Nevláknová se používá pro editační mód editoru, vláknová pro testovací režim, kdy je spuštěna hra přímo v editoru. GameLoop obsahuje metody pro přenášení zpráv mezi editorem a enginem. Každá zpráva implementuje rozhraní RDEditor.Message. Příklady zpráv mohou být StopEditMode, StartGameMode, NeedUpdate, DoOperation, UndoOperation a podobně. Poslední dvě jmenované vykonávají RDEditor.EditorOperation, což už jsou konkrétní operace s herními objekty – úprava pozice objektu, velikosti, rotace a podobně. Operace se dají přirovnat k úlohám (taskům), jen s tím rozdílem, že podporují Undo – navrácení do původního stavu. Díky tomuto přístupu je celý editor zaprvé jednoduše rozšiřitelný, zadruhé si sám řeší, kdy se operace vykonají (což je důležité kvůli podpoře více vláken) a zatřetí dovoluje ukládat historii operací a jejich vracení zpět (pokud to operace podporuje).
Kapitola 6
Závěr V celé práci jsem se snažil využívat moderních postupů jako je vícevláknové, task-based, eventy řízené a daty řízené programování. Aplikace je navržena jako maximálně rozšiřitelná. Implementace jádra zabrala obrovskou spoustu času, přesto (jak už to u softwarových projektů bývá) není stoprocentně hotová a doladěná. Funguje však většina stanovených požadavků, jádro je schopné pohánět jednoduché plošinové hry, obsahuje reálně vypadající fyzikální simulaci, umožnuje vykreslování i náročnějších grafických efektů a díky návrhu a podpoře pro skriptování je snadno rozšiřitelné. Editor splňuje zpočátku obtížně splnitelně vyhlížející požadavky a představuje tak funkční prototyp a základ, který by mohl být dále vyvíjen a vylepšován. Editor nabízí možnost přímého spuštění, ladění a debuggování hry – v těchto funkcích může být směle srovnáván s komerčními editory a vývojovými nástroji. Na druhou stranu možnosti editace levelu nejsou nikterak rozsáhlé, funguje základní manipulace s objekty, úprava herního terénu, generování a modifikace cest umělé inteligence, nastavování fyzikálních vlastností objektů nebo přehrávání cutscén. Editace skriptů probíhá v zabudovaném textovém editoru využívající komponentu s podporou obarvování syntaxe. Pro budoucí rozvoj aplikace by bylo dobré více rozdělit a zapouzdřit operace enginu do tasků, které by mohly být pro lepší rozložení zátěže zpracovávány paralelně. Dále by bylo dobré rozšířit možnosti editoru, přidat podporu skriptovatelného HUDu, přidat více možností umělé inteligence a podobně. V současné době existuje velice málo podobných nekomerčních knihoven a aplikací, proto pevně doufám a věřím, že vývoj aplikace neustane a ať už já nebo někdo jiný v něm bude nadále pokračovat.
45
46
KAPITOLA 6. ZÁVĚR
Literatura [1] 72% of US population are gamers, . http://www.mcvuk.com/news/30071/72-of-US-population-play-games, stav ze 19. 5. 2011. [2] Time spent gaming on the rise – NPD, . http://www.gamespot.com/news/6264092.html, stav ze 4. 5. 2009. [3] 2010 Total consumer spend on all games content int The U.S. estimated between $15.4 to $15.6 billion, . http://www.npd.com/press/releases/press_110113.html, stav ze 19. 5. 2011. [4] Video games bigger than film, . http://www.telegraph.co.uk/technology/video-games/68 52383/Video-games-bigger-than-film.html, stav ze 19. 5. 2011. [5] Češi a Slováci utratili loni za videohry přes dvě miliardy korun, . http://www.herniasociace.cz/2011/cesi-a-slovaci-utra tili-loni-za-videohry-pres-dve-miliardy-korun/], stav ze 19. 5. 2011. [6] BAFTA Video Games Awards, . http://www.bafta.org/awards/video-games/, stav ze 19. 5. 2011. [7] Inappropriate Content: A Brief History of Videogame Ratings and the ESRB, . http://www.escapistmagazine.com/articles/view/columns/the-n eedles/1300-Inappropriate-Content-A-Brief-History-of-Videog ame-Ratings-and-the-ESRB, stav ze 19. 5. 2011. [8] AHP vstoupila do mezinárodní organizace ISFE, . http://www.herniasociace.cz/2010/ahp-vstoupila-do-mezinarod ni-organizace-isfe/, stav ze 19. 5. 2011. [9] Sony Licenses Unreal Engine 3 For PS3, . http://www.digitalbattle.com/2006/07/21/sony-licenses-unrea l-engine-3-for-ps3/, stav ze 19. 5. 2011. [10] AHP. Jak vypadá typická hra? http://www.herniasociace.cz/hlavni-stranka/videohry-v-kostc e/jak-vypada-typicka-hra/, stav ze 19. 5. 2011. 47
48
LITERATURA
[11] BUCKLAND, M. Programming Game AI by Example. Wordware Publishing, Inc., 2004. In English. [12] IERUSALIMSCHY, R. Programming in Lua. Lua.org, 1st edition, 2003. In English. [13] IGDA. Study of Games And Development. www.igda.org/wiki/images/e/ee/Igda2008cf.pdf, stav ze 19. 5. 2011. [14] MCSHAFFRY, M. Game Coding Complete. Charles River Media, 3rd edition, 2009. In English. [15] R. IERUSALIMSCHY, W. C. L. H. d. F. Lua 5.1 Reference Manual. Lua.org, 2006. In English.
Příloha A
Výpisy kódu Výpis 7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
/ / jak by vypadala funkce Lua_ActorController::shoot po rozbaleni vsech maker int Lua_ActorController : : shoot ( lua_State ∗ l )
{
/ / (I) RD_LUA_ARGS(1); int __args = 1 ; int __argIndex = 1 ; int __return = 1 ; / / (II) RD_LUA_THISCALL(ActorController); const char ∗ __className Lua : : State L ( l ) ; L . openStack ( ) ;
= " ActorController " ;
try
{ Lua
: : S t a c k T a b l e _ _ t a b l e = _ _ g e t C l a s s T a b l e ( L , _ _ a r g s , _ _ c l a s s N a m e , ←__FILE__ , __LINE__ ) ;
ActorController ∗ __this __args , __className ,
= _ _ g e t T h i s ( L , _ _ t a b l e , ←__FILE__ , __LINE__ ) ;
/ / (III) RD_LUA_ARG_VECTOR3(target); V e c t o r 3 t a r g e t = L u a _ V e c t o r 3 : : f r o m T a b l e ( L , _ _ g e t T a b l e A r g ( L , ←_ _ a r g I n d e x ++, _ _ a r g s , f a l s e , _ _ c l a s s N a m e , _ _ F I L E _ _ , _ _ L I N E _ _ ) ) / / (IV) toto zustava stejne ref b u l l e t
= _ _ t h i s −>s h o o t ( t a r g e t ) ;
/ / (V) RD_LUA_RETURN_OBJECT(bullet); i f ( b u l l e t == N U L L )
{
}
L . getStack
( )−> p u s h N i l ( ) ;
else
49
;
50
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
PŘÍLOHA A. VÝPISY KÓDU
{ }
b u l l e t −>b o x T o S c r i p t ( L )
L . closeStack return
() ;
__return
;
;
/ / (VI) RD_LUA_END();
} / / zachyceni nekolika typu vyjimek, pro ukazku jen jedna c a t c h ( R D : : E x c e p t i o n& e )
{ }
L . closeStack throw e ;
() ;
L . closeStack () ; return __return
}
;
Výpis 8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
/ / ukazka komplexnejsi obalove funkce / / local objects = character:look(direction, 50); int Lua_Actor : : look ( lua_State ∗ l )
{
RD_LUA_ARGS (2) ; RD_LUA_THISCALL ( Actor )
;
RD_LUA_ARG_QUATERNION ( angle ) ; RD_LUA_ARG_DOUBLE ( maxDistance ) ObjectList objects _ _ t h i s −>l o o k ( a n g l e
;
; , objects , ( float ) maxDistance ) ;
L u a : : S t a c k T a b l e v a l u e = L . g e t S t a c k ( )−> c r e a t e T a b l e ( ) ; unsigned int i = 0 ; f o r ( O b j e c t L i s t : : i t e r a t o r i t = o b j e c t s . b e g i n ( ) ; i t != o b j e c t s . e n d ( ) i t ++)
{
value . setTable ( Utils : : Parse : : uintToString ( i ) ( ∗ i t )−> b o x T o S c r i p t ( L ) , true
);
}
}
i ++;
RD_LUA_RETURN_TABLE ( value ) RD_LUA_END () ;
;
. c_str ( ) ,
; ←-
51
Výpis 9 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
- - ukazka Lua skriptu reagujiciho na uzivatelsky vstup local gameController = ScriptedStateController local gameState = State . new ( " GameState " ) ; gameController . Callback = gameController ; gameState . Controller = gameController ; SystemManager SystemManager
. StateManager : addState ( gameState ) ; . StateManager : changeState ( " GameState " ) ;
- - lua nema prikaz switch, vypomuzeme si tabulkou keyPressed
= {};
function gameController : onKeyPressed ( e ) local cb = k e y P r e s s e d [ e . key ] ; i f c b ~= n i l t h e n c b ( e ) e n d ; end keyPressed keyPressed
. new ( ) ;
[ KeyCode . Escape ] = endGame ; [ K e y C o d e . Up ] = p l a y e r J u m p ;
f u n c t i o n endGame ( ) Game . Running = false
;
end f u n c t i o n playerJump ( ) level . Player . ActorControl end
: jump ( 1 . 0 ) ;
52
PŘÍLOHA A. VÝPISY KÓDU
Příloha B
UML diagramy
class Game
Game
«interface» GameloopListener «virtual» + onInit(Game*) + onUpdate(Game*, long) + onDestroy(Game*)
«virtual» + init() : bool + run() + isRunning() : bool + update() + stop() + destroy() + addListener(GameloopListener*) + removeListener(GameloopListener*) «Event» + InitializedEvent() + UpdatedEvent() + DestroyedEvent()
Obrázek B.1: Třída Game
53
54
PŘÍLOHA B. UML DIAGRAMY
class Graphics «interface» GraphicsData
«interface» Graphics
«virtual» + createData(Graphics*, SceneManager*) + afterCreateData(Graphics*, SceneManager*) + destroyData(Graphics*, SceneManager*) + afterDestroyData(Graphics*, SceneManager*) + synchronizeData(Graphics*, SceneManager*) + afterSynchronizeData(Graphics*, SceneManager*) «interface» TaskManager::Task
«virtual» + init() + destroy() + isInitialized() : bool + startRendering() + stopRendering() + isRendering() : bool + update() + getTaskManager() : TaskManager* + setTaskManager(TaskManager*) + getTargetFps() : float + setTargetFps(float) + getActualFps() : float + prepareSceneManager(SceneType, string) + getSceneManager(string) : SceneManager* + invalidateSceneManager(SceneManager*) + addData(GraphicsData*, SceneManager*) + removeData(GraphicsData*, SceneManager*)
«abstract» GraphicsTask
«virtual» + getName() : string + isProcessed() : bool + invalidate() + process()
«interface» Graphics::Listener
«Event» + RenderingStartedEvent() + RenderingStoppedEvent() + SceneManagerDestroyedEvent() + SceneManagerReadyEvent()
«virtual» + onFrameStarted(Graphics*, float) + onFrameEnded(Graphics*, float)
Obrázek B.2: Rozhraní Graphics
class TaskManager
«interface» Task + + + +
getName() : string isProcessed() : bool invalidate() process()
«interface» TaskManager::Listener + + + +
onTaskAdded(Task*) onTaskProcessed(Task*) onTaskAborted(Task*) onTaskScheduled(Task*)
«interface» TaskManager + + + + + + +
hasTask() : bool processOneTask() switchTaskQueue() abortTasks() addTask(Task*) addListener(TaskManager::Listener*) removeListener(TaskManager::Listener*)
Obrázek B.3: Struktura TaskManageru
55
class LevelObject
Trigger
PhysicsObject «virtual» + getPosition() : const Vector3& + setPosition(Vector3&) + getScale() : const Vector3& + setScale(Vector3&) + getOrientation() : const Quaternion& + setOrientation() + isInPhysics() : bool + configPhysics(PhysicsSystem*) + deconfigPhysics(PhysicsSystem*) + getBoundingRadius() : float + getBoundingBox() : AxisAlignedBox + hasPhysicsControl() : bool + removePhysicsControl() + getPhysicsControl() : PhysicsController* + setPhysicsControl(PhysicsController*) + getMass() : float + setMass(float) + getFriction() : float + setFriction(float) + getMaxVerticalSpeed() : float + setMaxVerticalSpeed(float) + getMaxHorizontalSpeed() : float + setMaxHorizontalSpeed(float) «Event» + OrientationChangedEvent() + ScaleChangedEvent() + PositionChangedEvent() + ConfiguredEvent() + DeconfiguredEvent() + NeedDeconfigureEvent() + NeedConfigureEvent()
+
Light
«interface» Actor
activate()
«Event» + ActivatedEvent()
LevelObject + + + +
isInLevel() : bool getParentLevel() : Level* getObjectID() : string setObjectID(string)
«virtual» + addedToLevel(Level *) + removedFromLevel() + resetTransforms() + showBoundingBox(bool) + getShowBoundingBox() : bool + getObjectType() : LevelObjectType # computeBounds()
«virtual» + hasActorControl() : bool + getActorControl() : ActorController* + setActorControl(ActorController*) + getDirection() : Direction + setDirection(Direction) + getHealth() : float + setHealth(float) + getMaxHealth() : float + setMaxHealth(float) + heal(float) + injure(float) + kill() + isAlive() : bool + getTeam() : Team + setTeam(Team) «Event» + DiedEvent() + HealedEvent() + InjuredEvent()
Model «virtual» + getMeshName() : string + setMeshName(string) + getMesh() : Entity + getAnimation(string) : Animation* + startAnimation(Animation*, bool) + stopAnimation(Animation*, bool) + finishAnimation(Animation*, float)
Character «virtual» + getAnimationDie() : Animation* + getAnimationShoot() : Animation* + getAnimationJump() : Animation* + getAnimationRun() : Animation*
Obrázek B.4: Struktura základních LevelObjektů
56
PŘÍLOHA B. UML DIAGRAMY
Příloha C
Obrázky
Obrázek C.1: Ukázka implementovaného editoru a jeho funkce pro úpravu cest umělé inteligence
57
58
PŘÍLOHA C. OBRÁZKY
Příloha D
Instalační a uživatelská příručka D.1
Spuštění hry
Cesta ke spustitelné hře je /compiled/bin/RainbowDemon.exe. Hra se ovládá klávesnicí a myší. Ovládání klávesnicí: • Šipka nahoru – Skok • Šipka vlevo – Chůze vlevo • Šipka vpravo – Chůze vpravo • Escape – Konec hry • R – Restart levelu Ovládání myší: • Levé tlačítko – Vystřelení paprsku • Pravé tlačítko – Držením se vytváří most
D.2
Spuštění editoru
Cesta ke spustitelnému editoru je /compiled/bin/RDEditor.exe. V editoru můžete načítat levely a skripty. Po načtení levelu můžete spustit hru tlačítkem v horní liště. Přehrávat cutscény můžete přes Level > Play Cutscene - jména cutscén jsou definována ve skriptech. V levé části je upravovat parametry levelu a vybraného objektu, včetně fyzikálních vlastností atd. Během editace skriptů můžete nastavovat breakpointy kliknutím nalevo od čísla řádku. Krokování aplikace probíhá přes tlačítka v horní liště. 59
60
PŘÍLOHA D. INSTALAČNÍ A UŽIVATELSKÁ PŘÍRUČKA
Ovládání klávesnicí: • Šipka nahoru/dolů – Posun kamery po ose Y • Šipka vlevo/vpravo – Posun kamery po ose X • Page Up/Page Down – Posun kamery po ose Z (zoomování) • Tab – Pokud je zvolen objekt s podporou editace (terén, cesty), spustí se nástroj pro editaci objektu • Q – Při editaci cesty: Zvolení nástroje pro posun bodů – Při editaci terénu: Zvolení nástroje pro posun bodů • W – Při editaci cesty: Zvolení nástroje pro přidávání bodů • C – Při editaci cesty: Spojení vybraných bodů • D – Při editaci cesty: Rozpojení vybraných bodů • Delete – Při editaci cesty: Odstranění vybraných bodů – Při editaci terénu: Odstranění vybraných bodů • E – Při editaci terénu: Zvolení nástroje Extrude • R – Při editaci terénu: Zvolení nástroje Split Ovládání myší: • Levé tlačítko – Manipulace s vybraným objektem/nástrojem • Pravé tlačítko – Vybrání level objektu/bodu terénu/atd. • Kolečko myši – Rolováním se zoomuje, držením posouvá kamera
Příloha E
Obsah přiloženého DVD Přiložené DVD obsahuje tento text ve zdrojovém formátu LATEXa vysázený dokument PDF. Dále obsahuje zdrojové soubory implementované aplikace a některé knihovny potřebné ke zkompilování. Součástí je i zkompilovaná spustitelná verze. Adresářová struktura: • / – /compiled
Zkompilovaná spustitelná verze
∗ /bin ∗ /media
Spustitelné EXE soubory Herní data (skripty, textury, modely...)
– /libraries ∗ ∗ ∗ ∗
Externí knihovny pro kompilaci
/boost /Bullet /Lua /OgreSDK
– /sources
Zdrojové soubory aplikace
∗ /RainbowDemon ∗ /uml
Zdrojové soubory editoru a herního jádra UML diagramy
– /text
Text BP
∗ /source
Zdrojový formát BP
61