text práce - Softwarové technologie a management

Transkript

text práce - Softwarové technologie a management
Č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 > 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<LevelObject>
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 <P a t h P l a n > 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 > 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 <R D : : L e v e l O b j e c t >() / / ziskani reference ref<RD::LevelObject>
−> 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 <R D : : L e v e l >() / / 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 <A c t o r C o n t r o l l e r >( 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 > 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

Podobné dokumenty

Programování počítačových her

Programování počítačových her Engine jako middleware 2/3 • Všechna studia v ČR používají vlastní engine • Proč mít vlastní engine:  je to naše (cena)‫‏‬  je to naše (vyznáme se v tom a dokážeme to upravit a vylepšovat)‫‏‬  ...

Více