4. týždeň

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.

Ciele

  1. Precvičiť si dedičnosť.
  2. Porozumieť prekrývaniu metód.
  3. Porozumieť princípu polymorfizmu.
  4. Naučiť sa používať kľúčové slovo super .
  5. Osvojiť si 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čí. Túto podobnosť môžeš v implementácii premietnuť do využitia dedičnosti, čo ti aj pomôže zredukovať opakujúci sa kód.

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

Zovšeobecnená implementácia nástroja, ktorý sa môže opotrebovať, zahŕňa sledovanie počtu ostávajúcich použití (stav) a aktualizovanie tohto počtu pri samotnom použití nástroja (správanie). Preto v triede BreakableTool vytvorte:

  • Č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.
  • Metódu use() na odrátavanie použití nástroja a na jeho odstránenie zo scény, keď počet ostávajúcich 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.

Upravte konštruktory týchto tried. Teraz, keď rodičovská trieda obsahuje len parametrický konštruktor, musíte ho vhodne zavolať pomocou kľúčového slova super.

Nezabudnite na zámer, kvôli ktorému sme vytvorili zovšeobecnenie v podobe rodičovskej triedy BreakableTool a upravte implementácie tried Hammer a FireExtinguisher tak, aby čo najviac využívali zdedenú funkcionalitu.

Ú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. Triedy môžete presunúť do nového balíka napr. pomocou drag&drop funkcionality v paneli so štruktúrou projektu. Vyhnete sa tak manuálnemu prepisovaniu package deklarácie na začiatku súborov a upravovaniu import-ov, keďže to dokáže prostredie vykonať automaticky.

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 pomocou Inšpektora. V tomto kroku však vykonáte potrebné úpravy na to, aby vedel reaktor pracovať automatizovane. Využijete na to systém akcií, ktoré budú reprezentovať činnosť alebo správanie aktéra v hernej scéne.

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

Gamelib

Akcia, reprezentovaná rozhraním Action a jeho všeobecnou implementáciou AbstractAction, slúži v hernej knižnici GameLib na zapuzdrenie činností aktérov do samostatných objektov. Naplánovaním akcie na scéne, do ktorej aktér patrí, sa začne jej (postupné) vykonávanie. Každá akcia naplánovaná na scéne, ktorá ešte nebola dokončená, sa vykoná volaním jej metódy execute() vždy pred vykreslením nového snímku scény. Pomocou akcií je možné implementovať správanie aktéra vzhľadom na plynúci čas, resp. reakcie aktéra na aktuálne dianie v scéne.

Popis priebehu hernej slučky v knižnici Gamelib nájdete aj na stránke jej dokumentácie.

Vo všeobecnosti, každá akcia má

  • stav reprezentovaný dvoma členskými premennými:
    • actor (getter getActor()) - aktér, s ktorým sa akcia vykonáva (referenciu nastaví scéna pri naplánovaní akcie, môže byť aj null)
    • isDone (getter isDone()) - vyjadruje, či sa akcia už ukončila (kontrolované scénou vždy pred vykonaním akcie).
  • dve podstatné metódy:
    • metóda execute(float deltaTime) - implementuje logiku akcie, je volaná vždy pred vykreslením nového snímku scény (max. 60x za sekundu) 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, ktorý dodá knižnica pri volaní tejto metódy.
    • metóda reset() - umožňuje resetovať stav akcie.

Plánovanie akcie sa vykonáva volaním metódy scheduleAction(Action action, Actor actor) nad objektom scény, alebo volaním pomocnej metódy scheduleFor(Actor actor) nad objektom akcie. To všetko si postupne ukážeme v nasledujúcich úlohách.

Úloha 2.1

V novom balíku sk.tuke.kpi.oop.game.actions vytvorte triedu PerpetualReactorHeating, ktorá bude reprezentovať akciu postupne zvyšujúcu teplotu 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. Trieda AbstractAction je dostupná v balíku knižnice sk.tuke.kpi.gamelib.framework.actions.

Trieda AbstractAction používa typový parameter A, ktorým je definovaný typ aktéra, s ktorým sa môže akcia vykonávať. Keďže má akcia zvyšovať teplotu reaktora, dosaďte jej typový argument Reactor:

public class PerpetualReactorHeating extends AbstractAction<Reactor> {

}

Poznámka

Časť implementácie triedy AbstractAction z knižnice vyzerá nasledovne:

public abstract class AbstractAction<A extends Actor> implements Action<A> {
    private A actor;

    @Override
    public A getActor() { return actor; }

    @Override
    public void setActor(A actor) { this.actor = actor; }

    /* ... zbytok triedy ... */
}

Všimnite si typový parameter **A**, ktorý

  • je ohraničený na typy odvodené od typu Actor (A extends Actor),
  • definuje typ členskej premennej actor ako aj typ návratovej hodnoty metódy getActor() a typ parametra metódy setActor().

Ak pri implementácii našej triedy PerpetualReactorHeating použijeme typ Reactor ako typový argument namiesto A (čo spravíme zápisom AbstractAction<Reactor>, ako je uvedené vyššie), dosiahneme to, že z pohľadu triedy PerpetualReactorHeating bude typ premennej actor, typ návratovej hodnoty metódy getActor() a typ parametra metódy setActor() zmenený na typ Reactor. Tým dosiahneme, že:

  • Akcia bude použiteľná len pre aktérov typu Reactor, keďže metóda setActor() bude akceptovať len argumenty typu Reactor - jej signatúra z pohľadu triedy PerpetualReactorHeating bude setActor(Reactor actor).
  • Volaním metódy getActor() dostaneme objekt typu Reactor a nie len všeobecného typu Actor. To nám umožní na tomto objekte volať metódy špecifické pre reaktor, ako napr. increaseTemperature(), čo budeme potrebovať v rámci ďalšej úlohy.

Ak to zhrnieme, tak podobne ako nám bežné parametre metód umžňujú zovšeobecniť implementáciu pre rôzne hodnoty, typové parametre nám umožňujú zovšeobecniť implementáciu pre rôzne typy.

Úloha 2.2

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

Pri implementovaní metódy execute() získajte referenciu na aktéra, s ktorým sa akcia vykonáva (bude to nejaký konkrétny 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 aktér 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 aktér 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 aktéra, 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 navrhol aj doplnkovú metódu scheduleFor() dostupnú priamo na akcii, ktorá akceptuje aktéra 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).scheduleFor(this);

Gamelib

Implementácia metódy scheduleFor() každej akcie je (zjednodušene) nasledovná:

Disposable scheduleFor(A actor) {
    return actor.getScene().scheduleAction(this, actor);
}

Ú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 aktéra za sebou, určite neuniklo vašej pozornosti, že v podstate 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. 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. Namiesto implementácie novej triedy využijeme akciu Invoke (dostupnú v knižnici GameLib), ktorou môžeme definovať činnosť akcie (to, čo sa vykoná v metóde execute()) aj referenciou na metódu implementujúcu 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. Keďže ide len o referenciu na metódu, túto metódu nevoláme, neodovzdávame jej žiadne argumenty, a preto ani nepíšeme zátvorky za názvom metódy. Prípadné argumenty sú metóde dodané až pri použití referencie.

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

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

Poznámka

Trieda akcie Invoke je univerzálna a využíva typový parameter pre typ aktéra, na ktorom môže byť konkrétna inštancia akcie naplánovaná. Pri použití triedy musíme teda buď

  • uviesť typový argument explicitne - vo vyššie uvedenom fragmente kódu by to bolo new Invoke<Reactor>(...), alebo
  • nechať kompilátor tento argument odvodiť z kontextu (ak je to monžé).

Odvodenie typového argumentu dosiahneme použitím diamantového operátora a zápis bude vyzerať new Invoke<>(...). Vynechanie tohto operátora na type s typovým parametrom kompilátor spracuje v režime spätnej kompatibility so starými verziami jazyka Java a označí taký typ ako raw typ.

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ť kompozíciou s akciou Loop (dostupná v GameLib), ktorá volá inú akciu danú ako argument jej konštruktora a nikdy nie je považovaná za skončenú (isDone() akcie Loop stále vráti false):

// v metode addedToScene triedy Cooler
new Loop<>(new Invoke<>(this::coolReactor)).scheduleFor(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ť. Je tu 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.

Krok 5: Repository

Úloha 5.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 ú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 (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 aktéra.

Doplňujúce zdroje