And... Action!

Motivácia

Kadet! Aby si obstál v boji s objektmi v nehostinnom svete Objektového programovania, potrebuješ sa zoznámiť s dôležitou vlastnosťou, ktorú niektoré objekty používajú na svoje maskovanie a oklamanie nepriateľa formou polymorfizmu. Touto vlastnosťou nie je nič iné, ako dedičnosť!

Náš taktický a strategický tím pre zdokonalenie tvojich schopností obstáť v tomto boji pripravil cvičnú misiu s kódovým označením Mission: Inheritance. Sú pre teba pripravené ciele, ktoré ti pomôžu porozumieť objektom využívajúcim túto vlastnosť. Po ich dosiahnutí sa tvoja šanca prežiť vo svete Objektového programovania dramaticky zvýši! Preto postupuj podľa pripraveného plánu misie.

Veľa šťastia pri zdolávaní cieľov! Z veliteľského mostíka 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.1.0 podľa tohto postupu.

Ciele

  • Dedičnosť
  • Prekrývanie metód
  • Polymorfizmus
  • Použitie kľúčového slova super.
  • Použitie referencie na metódu.

Postup

Krok 1: Similarity (not) entirely coincidental

Kadet! Nepochybuj o tom, že pôvod kladiva Mjolnir je jednoznačne extraterestriálny, a hoci oplýva nadľudskou mocou, chtiac-nechtiac má nejaké spoločné črty s hasiacim prístrojom. Dá sa použiť a časom sa zničí.

Abstraktná trieda BreakableTool reprezentujúca poškoditeľný nástroj.
Obr. 1: Abstraktná trieda BreakableTool reprezentujúca poškoditeľný nástroj.

Úloha 1.1

Vytvorte abstraktnú triedu BreakableTool, ktorá predstavuje zovšeobecnenie pre akýkoľvek pracovný nástroj, ktorý má obmedzený počet použití.

V triede nech je členská premenná remainingUses pre počet zostávajúcich použití nástroja, ktorú je potrebné sprístupniť zvonku triedy len na čítanie a inicializovať použitím parametrického konštruktora. Vytvorte metódu use() na odrátavanie použití nástroja a na jeho odstránenie z mapy, keď počet použití klesne na 0 (nástroj sa opotreboval a zlomil).

Poznámka

Pomôžte si kódom, ktorý ste implementovali už pri kladive alebo hasiacom prístroji.

Úloha 1.2

Upravte triedy Hammer a FireExtinguisher, aby boli potomkom triedy BreakableTool.

Nezabudnite na úpravu konštruktora týchto tried a odstránenie duplicitného kódu.

Úloha 1.3

Vytvorte nový balík sk.tuke.kpi.oop.game.tools a presuňte do neho triedy týkajúce sa nástrojov.

V projekte sa už zväčšuje počet tried. Aby ste si v nich zachovali poriadok a zoskupili spolu súvisiace triedy, vytvorte pre nástroje nový balík tools v balíku sk.tuke.kpi.oop.game a presuňte tam triedu BreakableTool, ako aj Hammer, Mjolnir a FireExtinguisher.

Poznámka

Pri tejto úlohe viete využiť možnosti refaktorizácie, ktoré ponúka vývojové prostredie. Vyhnete sa tak manuálnemu prepisovaniu package deklarácie na začiatku súborov a upravovaniu import-ov.

Krok 2: Self-exploding reactor

Doteraz ste mohli s reaktorom pracovať výlučne manuálne - vedeli ste ho zapnúť a vypnúť a vedeli ste mu zvýšiť a znížiť teplotu volaním vhodných metód. V tomto kroku však vykonáte potrebné úpravy na to, aby vedel reaktor pracovať samostatne. Využijeme na to systém akcií, ktoré budú reprezentovať činnosť alebo správanie actora v hernej scéne.

Vzťahy medzi actorom, scénou a akciou.
Obr. 2: Vzťahy medzi actorom, scénou a akciou.

Poznámka

Akcia, reprezentovaná rozhraním Action a jeho všeobecnou implementáciou AbstractAction, slúži v hernej knižnici GameLib na zapúzdrenie činností actorov do samostatných objektov. Naplánovaním vykonávania akcie na scéne, do ktorej actor patrí, potom dôjde k jej (postupnému) vykonaniu (realizácia návrhového vzoru Command). Všetky akcie naplánované na scéne, ktoré ešte neboli dokončené, sa vykonávajú vždy pred vykreslením nového snímku scény na obrazovku (v prípade štandardnej rýchlosti snímkov je to 60x za sekundu). Pomocou akcií je možné implementovať správanie actora vzhľadom na plynúci čas, resp. reakcie actora na aktuálne dianie v scéne.

Poznámka

Vo všeobecnosti má každá akcia
  • stav reprezentovaný dvoma členskými premennými:
    • actor (getter getActor()) - actor, s ktorým sa akcia vykonáva (referenciu nastaví scéna pri naplánovaní akcie)
    • isDone (getter isDone()) - vyjadruje, či sa akcia už ukončila (kontrolované scénou vždy pred vykonaním akcie).
  • dve podstatné metódy:
    • execute(float deltaTime) - metóda implementujúca logiku akcie, volaná vždy pred vykreslením nového snímku scény (v prípade, že akcia bola naplánovaná a nie je ešte ukončená). Parametrom metódy je čas (v sekundách) od posledného vykresľovania scény.
    • reset() - umožňuje resetovať stav akcie.

Úloha 2.1

V novom balíku sk.tuke.kpi.oop.game.actions vytvorte triedu PerpetualReactorHeating, ktorá bude reprezentovať akciu postupného zvyšovania teploty reaktora. Konkrétnu hodnotu kroku zvyšovania teploty umožnite špecifikovať pri vytváraní inštancie triedy v konštruktore.

Ako je možné vidieť aj z diagramu tried uvedenom vyššie, nová trieda akcie PerpetualReactorHeating má dediť od triedy AbstractAction z balíka sk.tuke.kpi.gamelib.framework.actions.

Poznámka

Trieda AbstractAction používa typový parameter, ktorým je definovaný typ actora, s ktorým sa môže akcia vykonávať. Keďže má akcia zvyšovať teploty reaktora, použite ako hodnotu typového parametra typ Reactor:
public class PerpetualReactorHeating extends AbstractAction<Reactor> {

}

Úloha 2.2

V triede PerpetualReactorHeating prekryte abstraktnú metódu execute(), v ktorej implementujete činnosť akcie.

Pri implementovaní metódy execute() získajte referenciu na actora, s ktorým sa akcia vykonáva (reaktor) a zvýšte jeho teplotu o hodnotu danú parametrom konštruktora akcie.

Úloha 2.3

Naplánujte vykonávanie akcie PerpetualReactorHeating na reaktore tak, aby sa teplota zvyšovala vždy o 1 stupeň.

Keďže pre naplánovanie akcie potrebuje actor poznať scénu, do ktorej patrí, vhodným miestom pre naplánovanie akcie môže byť metóda addedToScene() zdedená od triedy AbstractActor, ktorá v parametri dostane referenciu na scénu (typ Scene) a je zavolaná po tom, čo bol actor do danej scény pridaný. Metódu addedToScene() je teda potrebné prekryť. Nezabudnite však na to, že stále potrebujeme, aby sa pôvodná implementácia metódy addedToScene() z triedy AbstractActor vykonala, a teda využite kľúčové slovo super na zavolanie zdedenej implementácie!

Scéna definuje metódu scheduleAction(), ktorá akceptuje akciu a actora, s ktorým má akciu neskôr vykonávať. Naplánovanie vykonávania akcie v metóde reaktora môže preto vyzerať nasledovne:

// v metode addedToScene triedy Reactor
scene.scheduleAction(new PerpetualReactorHeating(1), this);

Náš taktický a strategický tím však odporúča využiť doplnkovú metódu scheduleOn() dostupnú priamo na akcii, ktorá akceptuje actora a zabezpečí to isté, ako metóda scheduleAction() na scéne, len s o niečo prehľadnejším zápisom:

// v metode addedToScene triedy Reactor
new PerpetualReactorHeating(1).scheduleOn(this);

Úloha 2.4

Overte správnosť svojej implementácie spustením reaktora.

Ak ste postupovali správne, zapnutému reaktoru sa bude postupom času zvyšovať teplota, čo si viete overiť pomocou inšpektora.

Krok 3: How cool is cool enough?

Teraz, keď máme prvý krok k vytvoreniu samočinného actora za sebou, určite neuniklo vašej pozornosti, že len na volanie jednej metódy reaktora bola vytvorená celá nová trieda. Tento nedostatok prvotného riešenia sa pokúsime postupne riešiť.

Úprava reaktora z predchádzajúceho kroku má však jeden háčik - tým, že necháte reaktor pracovať samostatne, bude sa systematicky prehrievať. A prehrievanie, ako vieme už od roku 1986, bude nevyhnutne viesť k jeho trvalému poškodeniu. Aby sme sa vyhli takejto situácii, namiesto kladív vytvoríte chladič, ktorý bude reaktor systematicky ochladzovať.

Vzťah tried Reactor a Cooler.
Obr. 3: Vzťah tried Reactor a Cooler.

Úloha 3.1

V balíku sk.tuke.kpi.oop.game vytvorte triedu Cooler pre chladič, ktorý bude zabezpečovať dostatočné chladenie pre pripojený reaktor.

Správanie chladiča a jeho vzťah s reaktorom opisuje nasledujúci diagram tried:

Trieda Cooler.
Obr. 4: Trieda Cooler.

Ako animáciu chladiča použite súbor fan.png. Keď bude chladič vypnutý, tak prehrávanie jeho animácie pozastavte. Keď chladič naopak zapnete, jeho animáciu spustite. Na objekte animácie nájdete vhodné metódy na jej ovládanie.

Animácia fan.png (rozmery sprite-u: _32x32_, trvanie snímku: _0.2_).
Obr. 5: Animácia fan.png (rozmery sprite-u: 32x32, trvanie snímku: 0.2).

Úloha 3.2

Do triedy Cooler pridajte privátnu metódu, v ktorej implementujete činnosť chladiča.

Metódu pomenujte napríklad coolReactor(). Pri jej zavolaní sa vždy (teda - ak je chladič zapnutý) zníži teplota pripojeného reaktora o jeden stupeň.

Úloha 3.3

Prekryte metódu addedToScene(), v ktorej naplánujete akciu pre činnosť chladiča.

Pre zavolanie metódy v rámci akcie nie je potrebné vytvárať celú triedu akcie, ktorá potom slúži len na jednu, veľmi špecifickú funkciu. Teraz však využijeme akciu Invoke (dostupnú v knižnici GameLib), ktorou môžeme definovať činnosť akcie (metóda execute()) aj referenciou na metódu implementujúcou požadované správanie.

Poznámka

Referencia na metódu method konkrétneho objektu object má v jazyku Java zápis v tvare object::method, používa sa teda operátor dvojitej dvojbodky.

Akciu a jej naplánovanie teda zapíšeme takto:

// v metode addedToScene triedy Cooler
new Invoke(this::coolReactor).scheduleOn(this);

Takáto akcia však celkom nesplní to, čo od nej čakáme: metódu coolReactor() zavolá iba raz a potom je považovaná za skončenú (isDone() vráti true). Opakované vykonávanie akcie vieme docieliť využitím akcie Loop (dostupná v GameLib), ktorá akceptuje inú akciu ako parameter konštruktora a stále je považovaná za neskončenú (isDone() akcie Loop stále vráti false):

// v metode addedToScene triedy Cooler
new Loop<>(new Invoke(this::coolReactor)).scheduleOn(this);

Poznámka

Akcia Loop implementuje návrhový vzor Decorator.

Úloha 3.4

Overte správnosť svojej implementácie spustením reaktora a pripojením najprv jedného a potom dvoch chladičov naraz.

Čím viac chladičov pripojíte, tým skôr bude reaktor ochladený. Pozor však na to, že reaktor nie je možné chladiť do mínusových teplôt.

Pracovnú teplotu reaktora práve zabezpečujú dva chladiče súčasne.
Obr. 6: Pracovnú teplotu reaktora práve zabezpečujú dva chladiče súčasne.

Krok 4: Defective Light

Náš analytický tím zadefinoval pokazené svetlo, ako svetlo, ktoré je pokazené. A keďže môžeme predpokladať, že pokazené svetlo bolo najprv v poriadku, jeho správanie odvodíme od dobrého svetla. V našom prípade sa bude pokazenosť prejavovať blikaním svetla v nepravidelných intervaloch.

Dedičná línia triedy DefectiveLight.
Obr. 7: Dedičná línia triedy DefectiveLight.

Úloha 4.1

V balíku sk.tuke.kpi.oop.game vytvorte triedu DefectiveLight, ktorá bude potomkom triedy Light.

Úloha 4.2

V triede DefectiveLight vytvorte metódu, ktorá bude definovať správanie pokazeného svetla.

Pre dosiahnutie efektu pokazeného svetla môžete použiť ľubovoľný postup. Jeden z nich môže vyzerať napr. tak, že vygenerujete náhodné číslo od 0 do 20 a ak bude vygenerované číslo 1, zmeníte stav svetla (zo zhasnutého na zasvietené a opačne).

Poznámka

Náhodné číslo môžte v Jave získať pomocou inštancie triedy Random alebo jednoducho volaním statickej metódy Math.random().

Úloha 4.3

Naplánujte správanie pokazeného svetla pomocou akcií Loop a Invoke.

Úloha 4.4

Overte správnosť svojej implementácie vytvorením pokazeného svetla a jeho pripojením k reaktoru.

Nezabudnite, že typ parametra metódy addLight() v triede Reactor netreba meniť. Práve tu je využitá vlastnosť polymorfizmu: DefectiveLight, ako potomok triedy Light, sa dá použiť všade tam, kde je akceptovaný typ Light.

Pokazené svetlo rozpoznáte od dobrého svetla na základe jeho činnosti.
Obr. 8: Pokazené svetlo rozpoznáte od dobrého svetla na základe jeho činnosti.

Doplňujúce úlohy

Úloha A.1

Vytvorte triedu SmartCooler predstavujúcu upravený Cooler, ktorý sa automaticky zapína a vypína v závislosti od teploty reaktora.

Pomocou smart chladiča zabezpečte udržiavanie pracovnej teploty reaktora v rozsahu od 1500 do 2500 stupňov. To znamená, že chladič bude chladiť len vtedy, ak aktuálna teplota stúpne nad 2500 stupňov. Ak však teplota klesne pod 1500 stupňov, chladič prestane reaktor chladiť.

V tomto prípade bude tiež zaujímavé sledovať, ako sa chladič bude zapínať a vypínať podľa toho, kedy bude naozaj pracovať.

Poznámka

Nezabudnite na to, že časť funkcionality chladiča je už riešená v triede Cooler. Vyhnite sa duplikovaniu kódu a využite možnosť volať metódy predka.

Úloha A.2

Vytvorte triedu Helicopter pre bojovú helikoptéru, ktorá bude vedieť prenasledovať hráča a zaútočiť na neho, keď ho dostihne.

V triede implementujte verejnú metódu searchAndDestroy(), ktorá spustí prenasledovanie hráča. Pohyb helikoptéry smerom k hráčovi pri pustenom prenasledovaní realizujte v metóde vytvorenej pre akciu Invoke. Zabezpečte, aby sa energia hráča znížila o 1, keď sa s helikoptérou stretne. Pre animáciu helikoptéry použite obrázok heli.png (stiahnite si ho do projektu do adresára src/main/resources/sprites).

Poznámka

Pri tejto úlohe neprekrývajte metódu addedToScene() v triede Helicopter! Porozmýšľajte, kde je vhodnejšie miesto pre naplánovanie akcie.

Poznámka

Pre získanie referencie na objekt hráča si prezrite dostupné metódy na objekte hernej scény. Hráč je typu Player a má meno Player. Pre overenie, či je helikoptéra v kolízii (dotýka sa) s hráčom, použite vhodnú metódu dostupnú na objekte actora.

Doplňujúce zdroje