8. týždeň

Usable Items

Motivácia

Kolonisti z planéty Acheron (LV-426) používali pri kolonizovaní planéty mnoho prístrojov a špeciálnych zariadení. Je možné predpokladať, že mnohé z nich budú ešte stále plne alebo aspoň čiastočne funkčné. Preto by k úspechu akcie počas tvojho výsadku na planéte mohlo pomôcť ich zvládnutie.

Technickí odborníci z nášho strategického a taktického tímu ťa teda dnes naučia tieto prístroje a zariadenia ovládať a pomôcť tak Ripleyovej ich vhodne používať.

Z operačného strediska zdraví Manager.

Ciele

  1. Použiť marker interface na odlíšenie podskupiny objektov.
  2. Precvičiť si generické programovanie.
  3. Naučiť sa použiť návrhový vzor Iterator .
  4. Naučiť sa vyvolávať a spracovávať výnimky.

Postup

Krok 1: Useful Rubbish

Podľa obrazu z niektorých funkčných kamier, prebiehali v priestoroch kolónie ťažké boje, po ktorých zostalo v miestnostiach množstvo rozhádzaných predmetov. Také lekárničky alebo zásobníky by sa mohli celkom hodiť...

Triedy Energy a Ammo implementujúce rozhranie Usable.
Obr. 1: Triedy Energy a Ammo implementujúce rozhranie Usable.

Úloha 1.1

Premenujte balík sk.tuke.kpi.oop.game.tools na sk.tuke.kpi.oop.game.items.

V rámci tohto kroku začneme do projektu pridávať ďalšie predmety, ktoré môžu byť pre Ripleyovú užitočné. Keďže niektoré z týchto predmetov budú zároveň nástrojmi, názov balíka tools zovšeobecníme na items.

Úloha 1.2

V balíku sk.tuke.kpi.oop.game.items vytvorte triedu Energy pre aktéra reprezentujúceho lekárničku. Nech implementuje rozhranie Usable.

Trieda Energy bude reprezentovať lekárničku, ktorá vždy povzbudí Ripleyovej náladu a doplní jej energiu na 100%. Túto funkcionalitu však budete implementovať neskôr.

Ako animáciu pre reprezentáciu lekárničky použite súbor energy.

Animácia energy.png (rozmery spritu: 16x16).
Obr. 2: Animácia energy.png (rozmery spritu: 16x16).

Úloha 1.3

Upravte triedu Ripley tak, aby ste mali k dispozícií getEnergy a setEnegry metódy pre stav energie.

Energia Ripleyovej bude reprezentovaná celým číslom. Využívať budeme rozsah 0 - 100 v zmysle percent zostávajúcej energie. Počiatočná hodnota energie nech je 100.

Úloha 1.4

V metóde useWith() triedy Energy implementujte doplnenie energie Ripleyovej na 100%.

Zabezpečte, aby po zmene stavu energie Ripleyovej bola lekárnička odstránená zo scény. Ak Ripleyová bude mať plnú energiu, k "použitiu" lekárničky nedôjde.

Úloha 1.5

V balíku pre akcie vytvorte novú akciu Use rozširujúcu triedu AbstractAction.

Pomocou akcie Use budete môcť plánovať použitie Usable aktéra iným aktérom, s ktorým (podľa signatúry metódy useWith()) môže byť daný Usable aktér použitý. Hlavnou činnosťou akcie Use teda bude zavolanie metódy useWith() na objekte Usable aktéra odovzdaného v konštruktore akcie. Argumentom volanej metódy useWith() bude aktér, na ktorom je akcia naplánovaná. Teda niečo ako:

usableActor.useWith(getActor());

Poznámka

Napríklad pri zámere použiť lekárničku s Ripleyovou (na doplnenie jej energie) by sme akciu Use použili takto:

new Use<>(energy).scheduleFor(ripley);

Použitie kladiva na opravu reaktora by zas vyzeralo takto:

new Use<>(hammer).scheduleFor(reactor);

Usable typ vvyžaduje typový argument. Aký? Pouvažujte o tom v súvislosti s príkladmi použitia uvedenými vyššie a vezmite do úvahy nasledovné:

Energy a iní Usable aktéri typovým argumentom odovzdaným Usable obmedzujú, s akým typom aktéra dokážu pracovať. Malo by byť preto možné použiť pri akcii Use len takú kombináciu argumentu konštruktora a aktéra v metóde scheduleFor(), ktorá by toto obmedzenie zachovala. Takže v konečnom dôsledku musí byť typ argumentu scheduleFor() rovnaký ako typový argument pre Usable, aby sme volali metódu useWith() so správnym typom argumentu. To dosiahneme definovaním typového parametra v akcii Use, ktorým tento typ spolupracujúceho aktéra vymedzíme, a ktorý odovzdáme aj do rodičovskej triedy AbstractAction.

Poznámka

Nezabudnite v metóde execute() zavolať metódu setDone(true) na signalizáciu ukončenia akcie.

Úloha 1.6

V rámci scenára naplánujte akciu, ktorá zabezpečí automatické použitie lekárničky v prípade, že sa dostane do kontaktu s Ripleyovou.

Skomponujte novú akciu Use s vhodnou existujúcou akciou.

Gamelib

Aby ste vedeli overiť, či k zmene stavu energie Ripleyovej naozaj došlo, pridajte zobrazenie stavu jej energie do okna hry. Viete to realizovať volaním metódy drawText() na grafickej vrstve Overlay nad zobrazenou hrou, ku ktorej sa dostanete zo scény takto:

scene.getGame().getOverlay();

Aby bol text energie horizontálne zarovnaný s textom FPS, použite v metóde drawText() y-ovú pozíciu (yTextPos) vypočítanú nasledovne:

int windowHeight = scene.getGame().getWindowSetup().getHeight();
int yTextPos = windowHeight - GameApplication.STATUS_LINE_OFFSET;

Hodnotu x-ovej súradnice nastavte podľa potreby.
Text zobrazený pomocou metódy drawText() je zobrazený len na 1 snímok. Keďže energia by sa mala zobrazovať neustále, potrebujeme zabezpečiť opakované volanie vykreslenia (vypísania) jej stavu. Docielime to buď naplánovaním s vhodnými akciami, alebo umiestnením vykresľovania v rámci triedy scenára do (prekrytej) metódy sceneUpdating(), ktorá je scénou volaná vždy pred vykreslením nového snímku.

Úloha 1.7

Overte správnosť svojej implementácie.

Dočasne upravte počiatočnú hodnotu energie Ripleyovej, aby ste overili správanie lekárničky. Tá by mala zmiznúť zo scény len ak Ripleyová pri prechode cez ňu nebude mať plnú energiu.

Ripleyová a lekárnička.
Obr. 3: Ripleyová a lekárnička.

Úloha 1.8

Podobným spôsobom ako v prípade Energy vytvorte v balíku sk.tuke.kpi.oop.game.items triedu Ammo a overte správnosť svojej implementácie.

Trieda Ammo bude reprezentovať zásobník, ktorý vždy povzbudí Ripleyovej náladu tým, že zvýši počet nábojov v jej zbrani.

Zabezpečte nasledovné správanie:

  • Vziať zásobník bude vedieť len inštancia triedy Ripley.
  • Množstvo nábojov bude zväčšené o 50.
  • Maximálne množstvo nábojov, ktoré Ripleyová unesie naraz vo vreckách, je 500.
  • Po tom, ako Ripleyová "použije" zásobník, tento odstráňte zo scény.
  • Pokiaľ bude mať Ripleyová plný stav nábojov, ku žiadnemu dobitiu ani odstráneniu zo scény nedôjde.

Ako sprite pre reprezentáciu zásobníka použite súbor s názvom ammo.

Sprite ammo.png (rozmery spritu: 16x16).
Obr. 4: Sprite ammo.png (rozmery spritu: 16x16).

Poznámka

Nezabudnite, že pre správne fungovanie triedy je potrebné zabezpečiť aj úpravu triedy Ripley. Vytvorte v nej preto členskú premennú na uchovávanie množstva nábojov a zodpovedajúci getter a setter pre prístup k nej.

Krok 2: !Samsonite Backpack

Vašou úlohou teraz bude podľa návodu, ktorý vypracoval náš taktický a strategický tím, vytvoriť batoh, ktorým bude Ripleyová vybavená. Tento batoh jej umožní mať poruke rôzne predmety, ktoré môže potrebovať v priebehu misie. Vypracovaný návod zároveň ukazuje spôsob, ako Ripleyovú naučiť rozpoznať predmety, ktoré sa do batoha zmestia.

Aby ste mali na čom stavať, náš technologický tím zároveň pripravil univerzálne rozhranie ActorContainer, ktoré predstavuje LIFO kolekciu určitého typu aktérov. Toto rozhranie by mal vami vytvorený batoh implementovať.

Trieda Backpack implementujúca rozhranie ActorContainer a rozhranie Keeper reprezentujúce aktérov s kontajnerom.
Obr. 5: Trieda Backpack implementujúca rozhranie ActorContainer a rozhranie Keeper reprezentujúce aktérov s kontajnerom.

Úloha 2.1

V balíku sk.tuke.kpi.oop.game.items vytvorte podľa uvedeného diagramu rozhranie Collectible, ktoré bude označovať predmety, ktoré bude možné ukladať do batoha.

Toto rozhranie bude rozširovať rozhranie Actor a nebude definovať žiadne ďalšie metódy. Bude teda slúžiť ako marker interface, ktorý v rámci typového systému definuje nový typ na odlíšenie skupiny objektov od ostatných - v tomto prípade na odlíšenie aktérov, ktorých bude možné vkladať do batoha.

Úloha 2.2

V balíku sk.tuke.kpi.oop.game.items vytvorte triedu Backpack, ktorá bude implementovať rozhranie ActorContainer z hernej knižnice.

Pri implementácii Backpack-u potrebujete konkretizovať typový argument použítého rozhrania ActorContainer. Keďže chceme, aby Ripleyová mohla prenášať v batohu len predmety na to určené, použite tu typ Collectible.

Batoh sa bude správať ako zásobník (LIFO). Pre ukladanie prvkov môžete využiť jednu z kolekcií, ktoré ponúka jazyk Java, napr ArrayList.

Konštruktor batoha bude mať nasledovnú signatúru:

public Backpack(String name, int capacity)
  • name reprezentuje meno batoha,
  • capacity definuje maximálnu kapacitu (teda počet predmetov, ktoré bude možné naraz v batohu niesť)

Úloha 2.3

V triede Backpack implementujte metódy getCapacity(), getContent(), getName() a getSize() z rozhrania ActorContainer.

  • getCapacity() bude predstavovať getter pre kapacitu batoha.
  • getContent() vráti kópiu zoznamu predmetov v batohu. Je dôležité, aby modifikácie tohto zoznamu neovplyvnili samotný obsah batoha!
  • getName() vráti názov kontajnera (batoha), ktorý sa v hre vykreslí popri jeho obsahu.
  • getSize() vráti aktuálny počet predmetov v batohu.

Poznámka

Pre vytvorenie kópie zoznamu môžete použiť vhodnú factory metódu dostupnú na rozhraní List.

Úloha 2.4

Vytvorte metódu add() rozhrania ActorContainer, pomocou ktorej budete vedieť vložiť do batoha nový predmet.

Pri implementácii metódy dodržte nasledovné správanie:

  • Predmety budú v kolekcii uložené v rovnakom poradí, v akom boli pridávané do batoha.
  • Do batoha bude možné vložiť maximálne toľko predmetov, koľko bolo zadaných pri jeho vytváraní argumentom capacity.
  • Ak bude batoh plný a aj napriek tomu bude metóda pre vloženie predmetu do batoha zavolaná, vyvolajte runtime výnimku IllegalStateException, ktorej v konštruktore odovzdáte správu "<backpack name> is full" kde <backpack name> nahradíte menom batoha.

Poznámka

Výnimka sa v Jave vyvoláva pomocou príkazu throw:

throw new IllegalStateException();

Úloha 2.5

Vytvorte metódu remove() rozhrania ActorContainer, pomocou ktorej odstránite predmet z batoha.

Úloha 2.6

Vytvorte metódu iterator() rozhrania ActorContainer, pomocou ktorej bude možné vrátiť referenciu na iterátor batoha.

Rozhranie ActorContainer rozširuje štandardné Java rozhranie Iterable<E>, ktoré definuje metódu iterator(). Implementáciou tejto metódy získame možnosť iterovať obsahom batoha prostredníctvom tzv. rozšíreného for cyklu, ako ilustruje nasledovná ukážka:

for (Collectible item : backpack) {
    // pouzitie predmetu (item) z batohu
}

Pri implementácii metódy postačí vrátiť iterátor dostupný z kolekcie, ktorá uchováva obsah batoha.

Úloha 2.7

V triede Backpack vytvorte metódu peek(), ktorá vráti referenciu na predmet, ktorý sa nachádza navrchu.

Batoh je reprezentovaný ako zásobník. Je dôležité, aby ste kedykoľvek vedeli získať referenciu na posledný vložený predmet, pretože toto bude predmet, s ktorým budete vedieť vykonávať rozličné operácie (napr. použiť ho alebo položiť na zem).

Úloha 2.8

Vytvorte metódu shift() rozhrania ActorContainer, ktorá posledne pridaný predmet presunie na dno batoha.

V kontajneri môžete vždy pracovať len s aktérom, ktorý je navrchu. Preto, ak budete potrebovať pracovať s niektorým aktérom vloženým hlbšie v kontajneri, potrebujete jeho obsah preusporiadať. Miesto toho, aby ste aktérov z kontajnera povyberali a povkladali ich späť v poradí, v ktorom potrebujete, ich môžete preusporiadať priamo v kontajneri pomocou jeho metódy shift().

Príklad použitia by teda mohol vyzerať nasledovne: ak sa v kontajneri nachádzajú aktéri v poradí D, C, B, A (od posledne vloženého po prvý vložený), tak po zavolaní metódy shift() bude poradie aktérov v kontajneri C, B, A, D.

Poznámka

Pre zmenu poradia predmetov v batohu môžete využiť vhodnú statickú metódu z utilitnej triedy Collections.

Úloha 2.9

V balíku sk.tuke.kpi.oop.game vytvorte rozhranie Keeper, ktoré bude rozširovať rozhranie Actor a bude reprezentovať aktérov vlastniacich Backpack.

Rozhranie pridá k aktérovi jednu nasledovnú metódu:

Backpack getBackpack();

Úloha 2.10

Upravte triedu Ripley tak, aby implementovala rozhranie Keeper a vybavte ju vytvoreným batohom Backpack.

Batohu nastavte kapacitu na 10 predmetov a môžete ho nazvať napríklad "Ripley's backpack".

Úloha 2.11

Upravte triedy Hammer, Wrench, FireExtinguisher tak, aby implementovali rozhranie Collectible.

Po tejto úprave sa objekty spomenutých tried budú dať vkladať do batoha.

Úloha 2.12

Zobrazte batoh Ripleyovej, naplňte ho aspoň troma rôznymi predmetmi a otestujte svoju implementáciu.

Gamelib

Obsah ActorContainer-ov je možné v hre aj graficky znázorniť (viď obrázok nižšie). V rámci scenára na to použite metódu pushActorContainer() nad objektom typu Game, ktorý získate zo scény metódou getGame().

V rámci scenára naplňte batoh troma predmetmi a skontrolujte, či posledne pridaný predmet sa bude nachádzať v zobrazenom batohu celkom vľavo. Skúste zavolať shift() metódu pre overenie preusporiadania predmetov.

Ripleyová vybavená batohom.
Obr. 6: Ripleyová vybavená batohom.

Krok 3: man backpack

V tomto kroku naučíme Ripleyovú používať vami vytvorený batoh. Vytvoríte preto niekoľko akcií, vďaka ktorým by to mohla Ripleyová bez problémov zvládnuť. Zaspomínate si na princíp polymorfizmu, v súlade s ktorým sa javí výhodné neobmedziť tieto akcie len pre Ripleyovú, ale implementovať ich pre akéhokoľvek Keeper-a.

Triedy akcií na prácu s kontajnermi aktérov.
Obr. 7: Triedy akcií na prácu s kontajnermi aktérov.

Poznámka


  • Keďže chceme zachovať možnosť kompozície akcií, všetky akcie na prácu s batohom Keeper aktéra budú mať typový parameter viazaný na typ Keeper, ktorý odovzdajú rodičovskej triede AbstractAction.
  • Nezabudnite zabezpečiť, aby vždy v rámci vykonávania metódy execute() došlo k nastaveniu isDone stavu akcie na true, keďže tieto akcie sú jednorázové.
  • Akcie vytvárajte v rámci balíka s akciami.

Úloha 3.1

Vytvorte akciu Take, pomocou ktorej bude možné vložiť do batoha predmet nachádzajúci sa na scéne v kolízii s Keeper aktérom vykonávajúcim akciu.

V metóde execute() nájdite v scéne prvého Collectible aktéra, ktorý je v kolízii s Keeper aktérom vykonávajúcim akciu. Ak takého nájdete, pridajte ho do batoha Keeper aktéra a odstráňte ho zo scény.

Poznámka

Ak si chcete vyskúšať niečo nové, tak na vyhľadanie Collectible aktéra, ktorý je v kolízií s Keeper aktérom, je možné použiť aj stream API.

Úloha 3.2

Ošetrite možný vznik výnimky IllegalStateException v metóde execute() akcie Take.

Keďže akcia Take pridáva aktérov do kontajnera, môže pri volaní mtódy add() dôjsť k vzniku výnimky IllegalStateException. Namiesto toho, aby v takom prípade aplikácia padla, budeme chcieť zobraziť správu z výnimky hráčovi na obrazovke. Preto je potrebné výnimku spracovať a vhodne odprezentovať.

Poznámka

V Jave je možné výnimky zachytávať v rámci bloku try a spracovať konkrétne typy výnimiek v nasledovnom bloku catch:

try {
  // kod ktory moze sposobit vynimku
} catch (Exception ex) {
  // spracovanie vynimky typu Exception
  // spravu vynimky ziskate metodou ex.getMessage()
}

Poznámka

V prípade vzniku výnimky pri plnom kontajneri neodstraňujte nájdeného aktéra v kolízii zo scény.

Gamelib

Správu z výnimky môžete zobraziť podobným spôsobom ako stav energie - do vrstvy Overlay. Aby sa však správa zobrazila napríklad na 2 sekundy, môžete využiť metódu showFor() objektu OverlayDrawing, korý získate z volania metódy drawText():

overlay.drawText(exception.getMessage(), x, y).showFor(2);

Úloha 3.3

Vytvorte akciu Drop, pomocou ktorej bude možné vyložiť aktéra z vrchu batoha do scény na miesto, kde sa nachádza Keeper aktér vykonávajúci akciu.

Pri implementácii metódy execute() vyberte z batoha aktéra, ktorý je na vrchu a umiestnite ho do scény tak, aby sa jeho stredová pozícia zhodovala so stredovou pozíciou Keeper aktéra, ktorý vykonáva akciu.

Úloha 3.4

Vytvorte akciu Shift, pomocou ktorej zabezpečíte zmenu poradia predmetov v kontajneri.

Úloha 3.5

V balíku sk.tuke.kpi.oop.game.controllers vytvorte triedu KeeperController implementujúcu rozhranie KeyboardListener. Tento ovládač bude slúžiť na plánovanie akcií Take, Drop a Shift na Keeper aktérovi pomocou klávesnice.

Referenciu na ovládaného aktéra získajte pomocou parametrického konštruktora. Ďalej prekryte metódu keyPressed() a implementujte plánovanie akcií na prácu s kontajnerom nasledovne:

  • Enter - vezme predmet do batoha.
  • Backspace - vyloží posledne vložený predmet z batoha.
  • S - posunie predmety v batohu, čím dôjde k zmene predmetu na vrchu batoha.

Úloha 3.6

Overte správnosť svojej implementácie.

Aby vám fungoval nový KeeperController na prácu s Ripleyovej batohom pomocou kláves, nezabudnite v rámci scenára vytvoriť inštanciu tejto triedy controller-a a nastaviť ho ako ďalší listener pre spracovanie vstupu.

Dôkladne vyskúšajte správnosť fungovania akcií pri stláčaní kláves, ktoré im zodpovedajú. Okrem základného fungovania kláves a akcií na prácu s batohom skontrolujte aj neobvyklé situácie, ako napríklad

  • stlačenie klávesy na pridanie predmetu do batoha, keď jeho kapacita je už naplnená,
  • stlačenie klávesy na vyloženie predmetu z batoha, keď je ten však prázdny,
  • stlačenie klávesy na zmenu poradia predmetov v batohu, keď je prázdny a potom keď je jeho kapacita naplnená.

Krok 4: Repository

Úloha 4.1

Nahrajte (cez commit a push) váš zdrojový kód do repozitára na GitLab-e. Spravte tak do nasledujúceho cvičenia. Zároveň si pripravte otázky, ktoré by ste na cvičení chceli vyriešiť.

Zdrojový kód nahrajte aj v prípade, ak ste nestihli dokončiť všetky úlohy. Rozpracované časti, ktoré by mohli spôsobiť kompilačné chyby, odporúčame zakomentovať.

Doplňujúce zdroje