Source Defender PPJ semestrální projekt Martin Přeták, ARI Source Defender je dynamická střílečka pro jednoho až dva hráče. Hráč má za úkol nabít všechny generátory kolem zdroje a ty pak nabité udržet až do výsledného nabití zdroje. V průběhu hry se však kolem zdroje teleportují nepřátelé a střelbou do generátorů je vybijí. Hráč tak musí zajistit řádné nabití generátorů a posléze držení jejích nabití než se zdroj aktivuje a všechny nepřátele zničí. Pokud do základny narazí více než jeden asteroid, hráč prohrál. Popis hry:
Ovládání Myš:
Levý nebo pravý klik – vystřelení prvního typu zbraně
Klávesnice:
A – posun proti směru hodinových ručiček B – posun po směru hodinových ručiček Mezerník – střelba druhého typu zbraně M – vyvolání menu hry
Uprostřed herní obrazovky je základna, okolo jsou vytvořené generátory, okolo generátoru se pohybuje hráč a nabijí je. Pokud generátor „dokuje“ tj. nemá žádné nabití, má zapnutý štít a nejde ho nijak dál zničit nebo vybít. Jakmile se hráč přesune nad generátor, začne se nabíjet. V průběhu nabíjení nemá generátor zapnutý štít a tak ho střelba nepřátel vybijí a tlačí zpátky k základně. Pokud se generátor plně nabije, zapíná štít. Tento štít je možno nepřáteli zničit, jakmile se ale hráč přiblíží k nabitému generátoru, automatický štít opravuje. Hráč, ani generátory není možno zničit. Hráč však má svoje životy. Pokud mu dojdou, spustí si štít a základna se postará o jeho vyléčení. Jediný hráčův zničitelný objekt je základna, která jde zničit pouze Asteroidem.
Zbraně Hráč má na ničení nepřátel dva druhy zbraní, střelba z každé zbraně vybijí hráčovu energii. První typ zbraně střílí jednotlivé silné střely (stisk myši), které však dost vybijí energii. Druhá typ zbraně jsou dvojité tenké střely (stisk mezerníku), které mají cca třetinový damage, střílí ale v rychlém sletu za sebou.
Nepřátelé Na hráče útočí tři druhy nepřátel: Fighter – jedná se o pojízdnou otočnou věž, krouží kolem základny a neustále odstřeluje hráče.
Bomber – jediný nepřítel, který je vybaven štítem. Ten však zapíná pouze pokud se pohybuje, jakmile dorazí na místo, štít se vypne a Bomber začne odstřelovat cíl. Má dvojitý damage oproti Fighterovi. Štít jde dobře zničit prvním typem zbraně.
Asteroid – jedná se o letící skálu namířenou přímo na základnu, při nárazu do ní ubere značný počet životů, základna vydrží náraz 1 asteroidu, při dalším nárazu se zničí a hráč prohrává. Pokud asteroid narazí do hráče, okamžitě vyvolá jeho štít.
Hráčův interface V levém dolním rohu vidí hráč svoji energii a životy:
Ukazatele jsou dvojité, mimo energie a životů hráče (vně) ukazují ještě energii a životy celé základny (uvnitř). Ukazatel energie základny se zobrazí až pokud jsou nabité všechny generátory a začne se nabíjet sama základna.
Natavení hry Valná většina konstant jde nastavovat v průběhu hry, některé se projeví okamžitě, pro jiné se musí daný objekt vytvořit znovu. Hru je možno jak ukládat tak načítat, ale pouze singleplayer. (Vyvolává se stiskem klávesy M)
Multiplayer Hra obsahuje i hru pro druhého hráče. K tomu je potřeba založit server. Do vytvořeného serveru se připojuje klient, což je úplně samostatný program. Klient pouze zobrazuje projekci stavu server (tj v jaké fázi se hra nachází, jaké objekty a jejich vlastnosti obsahuje atd.). Multiplayer obsahuje určité modifikace oproti singleplayeru. Hráči se nesmí křížit, to znamená, že přes sebe neprojdou, musí to vzít druhým směrem a sesynchronizovat svůj pohyb. Pokud se střetnou u sebe, automaticky se jim dobijí životy, zrychlí se také dobíjení energie a oba zároveň dobijí rychleji generátor, pokud jsou oba nad stejným. V multiplayeru je produkce nepřátel dvojnásobná.
Technologický popis hry Hra je rozdělena na server a klient. Server obsahuje veškerou logiku hry, singleplayer, multiplayer a síťový kód serveru. Klient implementuje pouze základní ovládání hry, síťový kód, tj. deserializuje data ze serveru a vytváří herní objekty. Ve hře je spousta dílčích tříd, na které by se dalo zaměřit a rozebrat je, pro základní popis fungování hry však budou stačit pouze následující informace.
GameEngine Jedná se o singletonovou třídu, která propojuje veškeré objekty, které nějakým způsobem mění hru. Pomocí GameEnginu se přidávají herní objekty do hry, mažou se, řídí se přepínání menu, vyvolávání oken, vytváří se server a posléze zpracovává jeho výstup, posílá mu svůj stav hry apod. Uchovávají se zde informace o všech herních objektech. Třída je také využívána herními objekty pro počítání kolizí, zjišťování nejbližších nepřátel atd.
GameObject extends Actor Třída představuje základní abstraktní herní objekt, každý objekt, který nějak interaguje, ukládá se a posílá se po síťi je potomkem této třídy. Třída představuje rozšíření klasického Actora, implementuje základní prvy každého herního objektu jako jsou životy, kolizní tělo, údaje o směru, rychlosti, pozici, jestli je zničený apod. Každý objekt je také určitého typu (GameObjectTypes) a má daného vlastníka (Owners). Každý herní objekt má také unikátní ID, které se zvětšuje v průběhu hry. Pomocí tohoto ID se pak rozlišuje, o jaký se jedná objekt při ukládání / načítání stavu hry a v síťové hře. Vlastnosti dead a destroy herního těla Každý herní objekt, pokud je zničený, informuje o tom okolí stavem atributu dead na true. Pokud je objekt „mrtvý“, GameEngine ho automaticky vymaže při další iteraci světa. Pokud objekt zabijeme (metoda kill(), popřípadě destroy()), dáváme tím najevo, že objekt už není dále potřeba.
Atribut destroy je zničení „napůl“. Defaultně GameObject ihned při zavolaní metody destroy volá i metodu kill a objekt je mrtvý. Někteří potomci (např Asteroid, PowerPlant, Bullet atp.) si implementují vlastní metodu destroy bez instantního killu metodou kill. Je to z toho důvodu, že objekt může být pro ostatní zničený, pro GameEngine však ne a objekt může dál fungovat například pro animaci smrti apod. Proč samostatné vlastnosti pozice, když už je implementuje Actor? Je to z toho důvodu, že pohyby ve hře jsou založeny na desetinných číslech, tím je umožněno plynulejšího všesměrového pohybu v jakékoliv rychlosti, objekt zároveň není závislý na tom, jestli je ve světě nebo ne.
CollisionBody extends Actor Jedná se o abstraktní třídu představující kolizní tělo herního objektu. Každý GameObject má svou soukromou instanci kolizního těla, nemusí ho však využívat. CollisionBody jako takové neřeší samotné počítání kolizí, řeší však kolize na úrovní kolizní vrstvy. Kolizní vrstva (CollisionLayers) je výčtový typ, který určuje, v jaké vrstvě se nachází kolizní tělo a se kterými dalšími vrstvami koliduje. Implementaci výpočtu kolize řeší až potomci kolizního těla (CircleBody).
PowerPlant Jedná se o herní objekt, který implementuje značnou část herní logiky (vyšší vrstvy funkcionalit). Jedná se o základnu, základna vytváří a spravuje generátory (PowerNode) a hráče (Torso, Turret), stará se o dohrání nebo vyhrání hry, dobíjení generátorů, pohybuje s hráči apod. PowerPlant je po GameEnginu klíčová třída pro celou hru.
Torso a Turret (hráč) Jedná se o třídy, které řeší hráče, a to co hráč ovládá. Torso se stará o otáčení celého a vytváří svůj Turret. Torso také vytváří vlastní štít, který zapíná, pokud má hp na 0. Turret, neboli věž poté implementuje otáčení a střelbu hráče.
Bullet Všechno ničení je ve hře zprostředkováno pomocí kulky. Kulka implementuje rozhraní IDamage a tím pádem může dávat damage a rozlišovat od koho přišel. Bullet jako takový nemá žádný obrázek, protože chování všech jeho druhů je vpodstatě totožné. Kromě kolizního těla, které určuje, s kým koliduje, obsahuje také informace o tom, koho může ničit, respektive komu při kolizi může udělit damage. Obsahuje také informace o tom, které vlastníky může ničit, popřípadě že by jich bylo více. Bullet vytváří třída BulletFactory, nastavuje kulce potřebné informace a obrázek, pro objekty je potom vytvoření otázka jedné metody.
Arcitektura ukládání hry Uložení hry má na starosti třída GameIO, ukládají se krátké segmenty bytů – DataSegment, využívá se zde ByteArray a FileChannel. Lze uložit jakýkoliv objekt, musí ale implementovat rozhraní ISaveable, které obsahuje metody pro získání uloženého DataSegmentu a rozparsování načteného DataSegmentu. Například GameObject toto rozhraní implementuje defaultně, poskytuje základní metodu pro vytvoření a načtení segmentu přímo v sobě. Zde je načteno / uloženo základní nastavení objektu (poloha, směr apod.), potomci si pak už jen přidávají svoje data.
GameIO Jak již bylo řečeno, třída se stará o lowlevel ukládání, v podstatě jen přijme seznam DataSegmentů a ty uloží, stejně tak při načítání. Načtená / uložená data jsou zapouzdřena v DataSegmentu, takže vůbec nemá ponětí o tom, co se ukládá.
DataSegment Jedná se o specifickou třídu, která vlastně „obaluje“ ByteArray a provádí nad ním potřebné operace. Veškerá data v segmentu jsou zapouzdřena, co se do něj vloží, již nejde odstranit a segment se finalizuje, tj. zablokuje proti dalšímu přidávání dat. Stejně tak vrací nemodifikovatelný ByteArray. DataSegment má hlavičku (12B) kde je uložena informace o tom, jaká třída se ukládá (classID) a jaká data obsahuje (dataID) a patičku kde je uložen kontrolní byte. Rozdíl mezi classID a dataID
classID je identifikátor třídy, tj. jaká třída načítá a ukládá data která jsou v segmentu. dataID je identifikátor typu dat, jednoduše je zde uloženo ID herního objektu, aby se vědělo i bez dat co je v segmentu. Herní objekty totiž potom při vytváření používají segmenty svých vnořených tříd pro získání správných dat.
Konečnou fázi vytváření herních objektů řeší GameEngine. Podle classID vytváří potřebné třídy a plní je DataSegmenty.
Arcitektura síťové hry Všechna data jsou postavena na javovské vlastnosti serializace. Posílají se kontejnerové objekty třídy NetworkSegment. V rané fázi obsahoval NetworkSegment ještě classID, z toho však sešlo, ale jako SuperClass zůstal, aby se rozlišilo, co všechno je segment pro síť. Každý objekt co chce posílat po síti musí také implementovat rozhrani INetworkable, které obsahuje metodu vracející právě NetworkSegment, nezáleží tím pádem na typu segmentu
každá třída může být „posílatelná“, záleží pouze na tom jak si segment implementuje. Z NetworkSegmentu dědí čtyři základní kontejnery.
GameStateSegment – obsahuje základní informace o stavu hry, informace o životech, energii druhého hráče a základny ManageDataSegment – obsahuje informace o ovládání druhého hráče ObjectStateSegment – obsahuje veškeré potřebné informace o herním objektu, který chceme poslat po síti FlashValueSegment – obsahuje informace o pozici efektu blesku, aby je hráč na druhé straně mohl řádně vykreslovat a nemusel se starat o jejich logiku
Síťová komunikace poté funguje tak, že se klientovi pošlou serializovaná data potřebných segmentů, klient data přijme, deserializuje a poté u sebe vytvoří. Klient na své straně běží mnohem rychleji než server, ale proud segmentů (respektive jejich interval posílání) udržuje hru ve stejné rychlosti jako na serveru.
Serverová část NetworkServer Třída implementuje klasický SocketServer, obstarává jeho vstupy / výstupy, stará se o přijetí klienta apod. Posílá klientovi segmenty a přijímá jediný ManageDataSegment s informacemi o ovládání druhého hráče. Klientovi se dále posílají všechny další segmenty (GameStateSegment, ObjectStateSegment a FlashValueSegment). NetworkServer vytváří GameEngine a stará se také o jeho chod.
Klientská část Pro připojení do hry je vytvořen samostatný program klienta. Jedná se o velice osekaný server, avšak základní premisa GameEngine a GameObject (zde NetGameObject) je zachována. GameEngine si zde stejně jako na serveru vytváří objekt NetworkClient a stará se o to, aby vše fungovalo, vyplňuje hráčův interface, zobrazuje stavy hry apod. Pro klientskou část je pak také stěžejní objekt třídy Torso, protože to je to jediné, co klient potřebuje – ovládat hráče. Torso / Turret jsou také osekány o všechno ostatní, zůstalo pouze ovládání. NetworkClient Stejně jako server se stará o základní síťovou funkcionalitu. Přijímá z klienta segmenty s aktualním stavem hry a potom volá metodu refreshGameState z GameObjectu, kde se všechny objekty vytváří a uchovává se stav hry. Serveru posílá pouze ManageDataSegment. NetGameObject Třída je rozdílná oproti klasickému GameObjectu. Hlavní informace, kterou nese je ObjectStateSegment, tedy stav objektu ze serveru. Třída není abstraktní, některé objekty
totiž nedělají nic než jen že „jsou“, to znamená, že nemusí být zděděny, ale pouze se jim přidělí obrázek. Jiné třídy jako například Shield, Explosion, Bullet apod. prochází během své existence různými stavy. Jak je řešen rozdílný stav objektů? Pro všechny stavy stačí celočíselný atribut state v ObjectStateSegmentu. Na straně serveru se při shromáždění dat tento stav nastaví a klient ho pak pouze zobrazí. Například štít má ve více stavech hry jinou průhlednost, tak se do atributu state vloží jeho průhlednost v aktuelní iteraci, stejně se řeší aktuální obrázek animace exploze apod. Na straně klienta si potom každý konkrétní potomek NewGameObjectu implementuje vlastní rozlišení stavu. NetGameObjectList Oproti GameObjectListu ze serveru je tohle zcela jiná třída. Určuje totiž kdo všechno bude v aktuální iteraci přítomen a kdo ne, neuchovává pouze herní objekty, zároveň je vytváří. Ze serveru dojde seznam ObjectStateSegmentů, ten se zde prochází, a segmenty s ID objektů, které ještě nejsou vytvořeny se vytvoří, ty, které již vytvořené jsou se zaktualizují a všechny přebývající odstraní. Takto je udržován trvalý stav hry stejný, jako je na serveru. Objekty se vytvářejí podle jejich typu (GameObjectType) a příslušná data se přiřazují těm co mají stejná ID. Samotný kód není pro rozsáhlost hry (nějak se to zvrtlo, ale prostě hry mě vždy bavilo dělat) a nedostatku času zdaleka dostatečně okomentován, snad to nebude vadit