Možná máte rádi počítačové hry tak, jako já. Možná dokonce i rádi hry programujete. Pokud ještě navíc programujete v jazyce C#, určitě jste museli narazit na jeden výrazný problém - nevyhovující knihovny. Nepodporovaná a pomalá XNA, zbytečně složitý MonoGame. S odpovědí přichází WaveEngine. Tvorba her vás začne opravdu bavit.
Ve WaveEngine je možné jednoduše a velmi rychle tvořit hry pro několik platforem najednou. Navíc obsahuje nejrůznější dárečky navíc, jako všelijaké obrazové efekty, prolínání přechodů mezi obrazovkami, podporu fyziky, snadnou tvorbu animací, particle systémy a spoustu dalších.
Pokusím se představit několik zásadních vlastností, kterými mě WaveEngine nejvíce zaujal. V celém článku budu WaveEngine představovat pouze z pohledu 2D her, jelikož 3D hry aktuálně netvořím a tolik jejich problematice nerozumím.
Stejně tak chci poznamenat, že existují i jiné enginy, ve kterých je možné tvořit hry v jazyce C# a je možné, že jsou v lecčems lepší. Pokud máte zkušenosti s některými z nich, určitě se o ně neváhejte podělit v diskuzi pod článkem.
Framework vs. Engine
Nejprve se pokusím vysvětlit rozdíl mezi enginem a frameworkem. MonoGame je pouhým frameworkem, stejně jako třeba knihovna PyGame v pythonu nebo LibGDX v jazyce Java či SDL v C++. To slovíčko „pouhým“ je zde voleno zcela záměrně.
Framework je totiž jakýmsi souborem nástrojů. Můžeme si jej představit jako dílnu plnou nářadí, prken, šroubků a hřebíků. V tu chvíli je jen na nás, jak přesně vyrobíme stůl. Na druhou stranu engine (z angličtiny motor) je v podstatě fungující fabrika, kde si jen správně zvolíme a nastavíme stroje, propojíme je pojízdnými pásy a potom zmáčkneme tlačítko a fabrika vyrábí stoly za nás.
V tvorbě her to znamená, že framework nám umožní dělat to, co potřebujeme, poskytne nám vhodné nástroje v podobě tříd a metod pro kreslení na obrazovku, uchovávání textur, posouvání obrázků, přehrávání zvuku a tak podobně, ale nechá nám naprostou svobodu v tom, jakým stylem budeme psát, kdy budeme provádět vykreslování a jak budeme zacházet s pamětí. Oproti tomu engine nám vnutí svůj způsob, na začátku hry jej tak nějak správně vyladíme, a typicky potom už stačí jen zapnout hru a engine vše zařídí za nás, včetně vykreslování, aktualizací herních postav a tak podobně. Zkrátka, méně volnosti, ale mnohem méně práce.
Enginů je ve světě her celá řada, od těch velice drahých od známých herních studií (jako CryEngine či Frostbite) přes dnes ohromně populární Unity až po ty méně známé nebo na ty, které se úzce specializují, například jen na 2D hry nebo třeba jen na RPG či textové hry. Spousta programátorů si také píše vlastní enginy, aby jim příště šla tvorba nové hry rychleji.
WaveEngine
WaveEngine je velmi mladý, vznikl teprve v roce 2013 a stále se velmi rychle vyvíjí a neustále dostává nové funkce. Pro nekomerční použití je zcela zdarma a klade si pouze jednu podmínku, kterouž je zobrazení „Splash screen“, tedy před začátkem hry musíte na několik sekund zobrazit logo WaveEngine, abyste dali najevo, že jste pro hru tento engine použili.
Výsledná hra lze spustit na mnoha různých platformách, nás asi zajímá nejvíce Windows, Linux, MacOS, iOS, Android a Windows Phone. Platformy Android a iOS, stejně jako MonoGame, běží pomocí knihovny Xamarin, a pro uveřejnění na Google Play nebo v AppStore je nutné si koupit licenci, která je v době psaní článku $25 měsíčně, což není úplně zadarmo, ale jak se říká, lepší než rezavým drátem do oka, nikdo nás do ničeho nenutí. Nicméně ta možnost tu je.
Architektura WaveEngine
Nejdůležitějším prvkem enginu je jeho architektura. Celá hra se skládá ze tří typů objektů:
- Scéna představuje jednu obrazovku ve hře, například menu, samotnou hru, nastavení, zobrazení výsledků atp. Scéna žije v samostatné třídě, která dědí od třídy Scene. V metodách Initialize a Start potom na scénu přidáváme nejrůznější entity.
- Entita reprezentuje samostatný objekt na scéně, od kamery a mapy přes postavu hráče, kolidující objekty (tedy okraje mapy či různé překážky ve hře), až po ukazatele času či skóre, tlačítka, a tak dále. Každá entita se potom skládá z několika komponent.
- Komponenty představují jakési orgány, údy a funkce entity. Například entitu hlavního hrdiny poskládáme z komponent Transform2D (pozice na mapě a velikost), Sprite (komponenta uchovávající texturu), SpriteRenderer (vykreslující texturu na mapu) a třeba ještě RectangleCollider (zjišťující kolize entity s jinými entitami) a RigidBody2D (tělo, které bude samo fyzikálně reagovat na ostatní entity s komponentou RigidBody2D). Nakonec hráče musíme nějak ovládat, k čemuž si vytvoříme vlastní komponentu PlayerBehavior.
Jak si uplácat entitu
Celá konstrukce entity pak může vypadat následovně:
var player = new Entity("player") // identifikátor entity
.AddComponent(new Transform2D()
{
X = 100, // počáteční pozice na obrazovce
Y = 100
})
.AddComponent(new Sprite("Content/player.wpk")) // speciální formát textury, který ale WaveEngine vytváří
// automaticky – stačí mít ve složce Content obrázek player.png
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha)) // vykreslovač na obrazovku
.AddComponent(new RectangleCollider()) // řešení kolizí
.AddComponent(new RigidBody2D() // fyzikální tělo
{
FixedRotation = true,
Mass = 100 // tělo bude vážit 100kg
})
.AddComponent(new PlayerBehavior()); // moje vlastní třída pro ovládání hráče
EntityManager.Add(player);
A to je vše. V tuto chvíli se nám na mapě bude vykreslovat hráč, bude kolidovat s ostatními objekty a bude ovladatelný tak, jak jsme nastavili ve třídě PlayerController. Není potřeba již cokoliv dalšího dělat. Engine sám aktualizuje a vykresluje všechny entity, my se nemusíme vůbec o nic starat. Žádná smyčka, žádné Update nebo Render metody.
Můžeme si všimnout taky jednoduchosti a krásy zápisu. Veškeré nastavení komponent je zpřístupněno pomocí properties, které lze specifikovat ve složených závorkách hned za konstruktorem, čímž se C# elegantně vyhýbá tzv. constructor hell, tedy milionu různých konstruktorů pro stejnou třídu. Díky tomu můžeme entitu z komponent poskládat jako stavebnici lega defacto jediným příkazem.
Systém závislostí komponent
Další pěkná vlastnost komponent je jejich systém závislostí. SpriteRenderer (vykreslovač 2D objektu) evidentně potřebuje pro správné vykreslení entity dostat jak texturu, tak pozici entity na obrazovce. Jenže my mu v příkladu ani jedno nepředáváme. Vlastně ho tvoříme úplně bez parametrů. A stejně to funguje. Čím to?
Komponenta A ve WaveEngine může „říct“, že ke svému správnému fungování musí být v její rodičovské entitě komponenta B, a díky tomu jí dostane k dispozici. Z komponenty B si potom může vytáhnout různá důležitá data.
Zde například komponenta Sprite vyžaduje komponentu Transform2D, protože si od ní přebírá rozměry entity a její pozici na obrazovce. SpriteRenderer potom vyžaduje komponentu Sprite. A vida, Sprite už obsahuje jak pozici, kterou si převzal od Transform2D, tak texturu, jejíž cestu jsme mu předali v konstruktoru. WaveEngine je celý na tomto principu postavený. Díky tomu u přidávání další komponenty musíme zadávat pouze nastavení, které je specifické pro tuto komponentu, a nestává se nám, že musíme cokoliv zbytečně opakovat. A z příkladu SpriteRendereru vidíme, že často komponentu stačí jen připlácnout do entity úplně bez parametrů.
Například RigidBody2D je komponenta, která představuje fyzikální objekt dané entity. Ke svému správnému fungování potřebuje RectangleCollider (nebo cokoliv zděděného od třídy Collider), který udává tvar a pozici entity, a my zde můžeme nastavit různé fyzikální konstanty jako je hmotnost, pružnost (Restitution) či tření (Friction).
Tvoříme vlastní komponenty
V předchozím příkladu jsme záměrně přeskočili komponentu PlayerBehavior. Teď je čas podívat se jí pod kapotu. V této komponentě nastavíme, jak se bude hráč chovat. Jelikož nebudeme tvořit nic vizuálního ani složitého, stačí nám dědit od třídy Behavior, která má jen dvě důležité metody – Initialize, ve které inicializujeme komponentu a která se automaticky volá při vytváření scény, a Update(TimeSpan), která se volá v každém cyklu hlavní smyčky a předává nám čas od posledního volání metody, a tedy v ní budeme řešit veškerou logiku a ovládání hráče.
Jak jsme si již řekli, komponenta může vyžadovat ke svému fungování jiné komponenty. My bychom rádi ve třídě PlayerBehavior například měnili naši pozici na základě stisků kláves, k čemuž potřebujeme komponentu Transform2D. Kde ji sehnat? To se zařídí naprosto snadno. Stačí do třídy přidat veřejnou proměnnou:
[RequiredComponent]
public Transform2D Transform2D;
Těm hranatým závorkám se říká v C# atributy, v jiných jazycích je najdeme v jiném formátu také pod názvem anotace, a jde o metainformaci, která má v kódu nějaký speciální význam. V tomto případě plní roli Dependency Injection (vkládání závislostí) – automaticky nám do proměnné vloží komponentu s daným typem, která se v rodičovské entitě vyskytuje. Pomocí této techniky si můžeme ve vlastních komponentách snadno nadiktovat, co vše potřebujeme, a zároveň tuto komponentu zpřístupníme a můžeme ji ve třídě rovnou začít používat.
Na co dalšího se těšit
Do enginu se dostalo opravdu hodně funkcí, které nám programátorům neskutečně usnadňují život. V základu již je vyřešena logika přepínání mezi scénami (třída WaveServices.ScreenContextManager), a k tomu jako bonus máme k dispozici více než 20 různých efektů přechodů mezi nimi, jako je prolnutí obrazu atp.
Skvěle je řešeno i rozlišení na různých zařízeních, s čímž nám pomůže třída WaveServices.ViewportManager. K tomu stačí jediný řádek kódu:
WaveServices.ViewportManager.Activate(800, 600, ViewportManager.StretchMode.Uniform);
V tuto chvíli nám stačí vyvíjet v rozlišení 800x600 pixelů, a ViewportManager za nás zařídí, že se všude obraz správně přizpůsobí, například zde v módu StretchMode.Uniform se rovnoměrně roztáhne/zúží podle potřeby.
Stejně jednoduše, pomocí jediného řádku, si můžeme uložit libovolnou instanci třídy do lokálního úložiště, a to zcela nezávisle na zařízení:
MyStorageClass storage;
WaveServices.Storage.Write<MyStorageClass>(storage);
A elegantní je i její načtení:
if (WaveServices.Storage.Exists<MyStorageClass>())
{
storage = WaveServices.Storage.Read<MyStorageClass>();
}
Podobně snadno dostaneme spoustu dalších věcí včetně zpožděných událostí (za pomocí delegátů), 2D paralaxové posouvání obrazovky či nejrůznější efekty obrazu zvané ImageEffects.
Pro někoho může být zajímavá taky podpora formátu TiledMap, 2D animačního nástroje Spine nebo podpora helmy Oculus Rift.
Shrnutí
Tím končí naše lehká ochutnávka, jak moc vám může WaveEngine usnadnit život a pomoci zaměřit se na samotné herní mechanismy, místo opakovaného vynalézání kola. S WaveEnginem je opravdu radost hry tvořit a já, byť obvykle velmi náročný uživatel, můžu tento nástroj s radostí doporučit.
Zásadním nedostatkem WaveEngine je, vzhledem k jeho mladému věku, poměrně malá komunita, a také nedostatečné množství tutoriálů a návodů. Druhý zmíněný nedostatek je poměrně dobře vyvážen velkým množstvím ukázkových her – Samples. Navíc samotný kód je samovysvětlující a s pomocí Intellisense ve Visual Studiu často není žádná dokumentace potřeba. Pro základní přehled o enginu taky doporučuji projít zveřejněné prezentace, kde jsou hezky popsané různé vlastnosti enginu.
Kdyby vás knihovna zaujala a byl by zájem, nebránil bych se vytvoření tutoriálu představujícím jednotlivé aspekty enginu zevrubněji a třeba bych ukázal, jak vytvořit nějakou základní hru typu Bomberman. Pokud máte jakékoliv dotazy, neváhejte se ozvat v diskuzi, rád odpovím.