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.

Upozornenie

Skôr než začnete prácu na tomto cvičení, aktualizujte si hernú knižnicu, ktorú v projekte používate, na verziu 2.4.0 podľa postupu na konci stránky o knižnici.

Táto verzia obsahuje aj zmeny, ktoré spôsobia vznik varovaní pri kompilácii existujúceho kódu: Akcie Invoke a Wait majú typový parameter, ktorý ich umožní lepšie komponovať s inými akciami do sekvencie. Typový argument bude v prevažnej väčšine prípadov automaticky odvodený kompilátorom, avšak všetky volania ako new Wait(...) a new Invoke(...) je potrebné prepísať na new Wait<>(...) a new Invoke<>(...).

Spôsob ako rýchlo opraviť všetky varovania je nasledovný:
  1. Aktualizujte knižnicu a importujte zmeny cez Gradle.
  2. V IDE z menu Analyze spustite akciu Inspect code nad celým projektom.
  3. Vo výstupe (otvorí sa panel v spodnej časti okna) vyriešte problémy v sekcii Java / Compiler issues.

Ciele

  • Použiť marker interface na odlíšenie podskupiny objektov
  • Precvičiť si generické programovanie
  • Naučiť sa použiť návrhový vzor Iterator
  • 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

Pomocou refaktorizácie 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, balík tools zovšeobecníme na items.

Úloha 1.2

V balíku sk.tuke.kpi.oop.game.items vytvorte triedu Energy, ktorá bude potomkom triedy AbstractActor a bude implementovať 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.png.

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í get a set 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.

Akcia Use bude mať konštruktor s jedným parametrom, ktorý bude typu Usable actora použiteľného s nejakým actorom. Actor, pre ktorého sa daná Use akcia naplánuje, bude, samozrejme, dostupný z metódy getActor() (resp. nastavený volaním metódy setActor() scénou).

Poznámka

Napríklad pri zámere použiť lekárničku s Ripleyovou (na doplnenie jej energie), čo by bolo reprezentované volaním energy.useWith(ripley) v execute() metóde akcie, by sme akciu Use použili takto:
new Use<>(energy).scheduleOn(ripley);
Použitie kladiva na opravu reaktora (volanie hammer.useWith(reactor) v execute() metóde akcie) by zas vyzeralo takto:
new Use<>(hammer).scheduleOn(reactor);

Energy a iní Usable actori typovým argumentom odovzdaným Usable obmedzujú, s akým typom actora dokážu pracovať. Malo by preto byť možné použiť pri akcii Use len takú kombináciu argumentu konštruktora a actora v metóde scheduleOn(), ktorá by toto obmedzenie zachovala (teda že v konečnom dôsledku by sme volali useWith() metódu so správnym typom argumentu). To dosiahneme použitím typového parametra aj v akcii Use, ktorým tento typ spolupracujúceho actora vymedzíme.

Poznámka

Trieda AbstractAction má tiež typový parameter, nezabudnite ho preto vhodne konkretizovať: musí to byť actor takého typu, ktorý chceme, aby bol akceptovateľný v metóde scheduleOn() a ako návratová hodnota getActor().

Poznámka

Nezabudnite v metóde execute() vyvolať použitie použiteľného predmetu za pomoci actora vykonávajúceho akciu a potom 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.

Poznámka

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() nad grafickou vrstvou 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 topOffset = GameApplication.STATUS_LINE_OFFSET;
int yTextPos = windowHeight - topOffset;
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. Docielite 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.png.

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 actor-ov. Toto rozhranie by mal vami vytvorený batoh implementovať.

Trieda Backpack implementujúca rozhranie ActorContainer a rozhranie Keeper reprezentujúce actorov s kontajnerom.
Obr. 5: Trieda Backpack implementujúca rozhranie ActorContainer a rozhranie Keeper reprezentujúce actorov 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 actorov, 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.

Poznámka

Rozhranie ActorContainer sa nachádza v hernej knižnici.

Pri implementácii Backpack-u potrebujete konkretizovať typový parameter 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čtu 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 vlastnú 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 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.

Týmto realizujeme návrhový vzor Iterator.

Ú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 actorom, ktorý je navrchu. Preto, ak budete potrebovať pracovať s niektorým actorom vloženým hlbšie v kontajneri, potrebujete jeho obsah preusporiadať. Miesto toho, aby ste actorov 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ú actor-i v poradí D, C, B, A (od posledne vloženého po prvý vložený), tak po zavolaní metódy shift() bude poradie actor-ov 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ť actorov vlastniacich nejaký ActorContainer.

Rozhranie bude mať typový parameter viazaný na typ Actor, ktorým sa vymedzí typ actorov v kontajneri.

Rozhranie pridá k actorovi jednu nasledovnú metódu:

ActorContainer<A> getContainer();

kde A reprezentuje spomínaný typový parameter.

Ú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.

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).

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 na prácu Ripleyovej s batohom, ale implementovať ich pre akéhokoľvek Keeper-a vlastniaceho akýkoľvek typ kontajnera ActorContainer.

Akcie by mali byť prispôsobené na akýkoľvek typ actor-ov v kontajneri. Preto ich vytvoríme s typovým parametrom, ktorým budeme môcť typ actor-ov konkretizovať v triedach vytváraných akcií.

Triedy akcií na prácu s kontajnermi actorov.
Obr. 7: Triedy akcií na prácu s kontajnermi actorov.

Poznámka

Všetky akcie vytvárajte v rámci balíka s akciami ako podtriedy AbstractAction.

Úloha 3.1

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

Kompatibilným predmetom sa myslí predmet takého typu, ktorý je možné uložiť do kontajnera vlastneného Keeper actorom. Trieda akcie Take tak bude opäť využívať typový parameter, ktorý bude reprezentovať typ kompatibilných actorov. Na základe tohto parametra bude potrebné aj vhodne dodefinovať typ Keeper actora, ktorý môže akciu vykonávať (ktorý bude odovzdaný ako typový parameter pre AbstractAction).

Deklarácia konštruktora akcie bude vyzerať nasledovne:

public Take(Class<T> takableActorsClass)

kde:

  • T zodpovedá typovému parametru popísanému vyššie,
  • takableActorsClass typu Class<T> je referencia na runtime reprezentáciu triedy actorov, ktorých je možné vložiť do batoha.

V metóde execute() nájdite v scéne prvého actora, ktorý je v kolízii s Keeper actorom vykonávajúcim akciu, a ktorý je zároveň kompatibilného typu. Ak takého nájdete, pridajte ho do kontajnera Keeper actora a odstráňte ho zo scény.

Poznámka

Pri implementácii metódy budete potrebovať kontrolovať typ actorov nachádzajúcich sa na scéne. V dôsledku obmedzenia JVM platformy sa však typové parametre nedajú použiť s instanceof. Runtime reprezentácia triedy actora kompatibilného s kontajnerom (parameter konštruktora) však má nasledovné metódy, ktoré viete využiť:
  • isInstance() namiesto instanceof
  • cast() namiesto bežného pretypovania.

Poznámka

Vyhľadanie kompatibilného actora, ktorý je v kolízií s Keeper actorom, je možné prehľadne vyriešiť použitím stream API.

Poznámka

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 akcia Take je jednorázová.

Úloha 3.2

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

Keďže akcia Take pridáva actorov 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, však 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
}

Poznámka

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

Poznámka

Správu z výnimky získate metódou getMessage() a môžete ju 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ť actora z vrchu kontajnera do scény na miesto, kde sa nachádza Keeper actor vykonávajúci akciu.

Signatúra triedy bude obdobná ako pri akcii Take. Keďže ale pri tejto akcii nebudeme potrebovať vykonávať instanceof kontroly nad typovým parametrom, konštruktor akcie bude bezparametrický.

Pri implementácii metódy execute() vyberte z kontajnera actora, ktorý je na vrchu a umiestnite ho do scény tak, aby sa jeho stredová pozícia zhodovala so stredovou pozíciou Keeper actora, ktorý vykonáva akciu. Nezabudnite zabezpečiť ukončenie akcie.

Poznámka

Pri používaní tejto akcie (pri vytváraní inštancií) bude potrebné špecifikovať typový argument pre typ actorov v kontajneri explicitne, keďže typový systém ho nebude mať na základe čoho odvodiť. Konštruktor akcie je totiž bezparametrický.

Úloha 3.4

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

Keďže v akcii Shift len meníme poradie prvkov kontajnera pomocou existujúcej bezparametrickej metódy, akcia nepotrebuje priamo pracovať s actormi v kontajneri a nepotrebuje tak poznať ani ich konkrétny typ. Trieda akcie teda nepotrebuje typový parameter a typ actorov kontajnera v typovom argumente AbstractAction môžeme nahradiť zástupným znakom "?".

Konštruktor akcie bude opäť bezparametrický a pre metódu execute() opäť platí potreba zmeny stavu akcie na ukončenú.

Úloha 3.5

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

Referenciu na ovládaného actora 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ý CollectorController 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á.

Doplňujúce úlohy

Úloha A.1

KeyboardController vytvorený na minulom cvičení premenujte na MovableController a presuňte do balíka pre sk.tuke.kpi.oop.game.controllers.

Dôvod premenovania je, že daný ovládač bude slúžiť len na ovládanie pohybu a MovableController bude teda výstižnejší názov.

Doplňujúce zdroje