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
- 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ť...
Ú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.
Ú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.
Ú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.
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ť.
Ú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.
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.
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 typKeeper
, ktorý odovzdajú rodičovskej triedeAbstractAction
. - Nezabudnite zabezpečiť, aby vždy v rámci vykonávania metódy
execute()
došlo k nastaveniuisDone
stavu akcie natrue
, 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
- Návrhový vzor Iterator
- Návrhový vzor Marker Interface
- Utilitná trieda Collections
- Rozšírený
for
cyklus - Výnimka IllegalStateException
- Java stream API