Grafický projekt 4

V predchádzajúcej kapitole sme vytvorili základ prostredia s postavičkami na bielom pozadí. Zastavili sme sa pred problémom ovládania postavičiek. Tým sa budeme zaoberať v tejto kapitole.

V triede AnimovanýObjekt sme definovali metódu myšV, ktorú sme využili v metóde kresliTvar. Možno ste si všimli, že keď počas presúvania sa postavičiek z domovskej pozície na svoje kotviace pozície na brehu prejdete myšou ponad niektorú z nich, objaví sa nad jej hlavou popis. Len čo sa všetky animované objekty zastavia, tento mechanizmus prestane fungovať. Je to preto, že svet robota nemá dôvod prekresľovať grafiku, keď nie je žiadny robot aktívny. Mechanizmus popisov v skutočnosti funguje naďalej, ibaže to nevidíme. Situáciu môžeme vyriešiť aktiváciou hlavného robota.

Poznámka: hlavný robot je vždy prvý robot vytvorený vo svete grafických robotov. Pre nás je výhodné, ak ním je robot hlavnej triedy. Preto sme žiadny z animovaných objektov nedefinovali staticky. Máme tak istotu, že prvým robotom bude priamo robot hlavnej triedy. Možno ste si ho už všimli. Ak ste videli typický tvar trojzubca po celý čas nehybne stojaci uprostred plátna, tak to je on.

Na konci konštruktora hlavnej triedy prikážeme:

aktivuj();

Po spustení hry bude zobrazovanie a miznutie popisov nad postavičkami fungovať aj potom, čo sa všetky zastavia. Zopakujme si teraz, ako pracuje metóda myšV naprogramovaná v predchádzajúcich kapitolách. Použili sme ju na zobrazovanie popisov nad hlavami postavičiek a budeme ju potrebovať pri ovládaní hry myšou. Metóda myšV má za úlohu overiť, či sú súradnice ukazovateľa myši v rámci plochy grafiky animovaného objektu. Ide o relatívne primitívny spôsob overenia – metóda pracuje jednoducho: iba vyhodnocuje, či sa kurzor myši nachádza v ploche pomyselnej elipsy so stredom v strede objektu. Ak chceme presne vidieť, ktorá plocha je považovaná za plochu objektu, jednoducho nakreslíme rovnakú elipsu v rámci kreslenia vlastného tvaru animovaného objektu. Pozrime sa do metódy myšV v triede AnimovanýObjekt. Tu vidíme rozmery pomyselnej elipsy: myšVElipse(50, 60). Pridajme na začiatok metódy kresliTvar (v rámci tej istej triedy) nasledujúce dva príkazy:

farba(0, 0, 100, 100);
vyplňElipsu(50, 60);

Po spustení hry, uvidíme pri (resp. na alebo pod – záleží kam príkaz kreslenia umiestnite) všetkých objektoch elipsy vyplnené priesvitnou modrou farbou:

{Súbor: figures-01.png}

Elipsa je pre každý objekt rovnaká a nie celkom presne vystihuje jeho rozmery a tvar. Keby sme chceli byť presní, museli by sme pre potreby metódy myšV vytvoriť tzv. klikaciu mapu, v ktorej by boli aktívne len reálne viditeľné body postavičky (na obrazovke). Nebudeme však komplikovať spôsob fungovania tejto metódy, iba mierne upravíme rozmery elipsy pre niektoré objekty, aby približne pokryli ich tvar. Pre vlka a kozu nám tvar elipsy celkom vyhovuje. Inak je to pri kapuste a prievozníkovi. Kapusta je menšia, viac­‑menej kruhová a potrebovali by sme elipsu posunúť o niečo nižšie, aby lepšie pokryla rozmery kapusty. Skúsme najprv odhadnúť veľkosť a umiestnenie elipsy použitej pre kapustu. Zmeňme príkazy, ktorými kreslíme priehľadnú modrú elipsu takto:

farba(0, 0, 100, 100);
skoč(0, -20);
vyplňElipsu(50, 50);
skoč(0, 20);

Prvým príkazom skoč sme posunuli stred elipsy o niečo nižšie a druhým sme posunutie vrátili, aby sa správanie ostatných príkazov nijako nezmenilo. Vrátenie súradníc na pôvodné hodnoty je dôležité. Ak by sme na to zabudli, hra by sa mohla začať správať „čudne“…

Spustime hru a nevšímajme si to, že nakreslená elipsa (resp. kruh) sa zmenila pre všetky objekty. Ide len o vizualizáciu – správanie metódy myšV sme predsa nemenili a všetky objekty reagujú tak ako doteraz. Kreslenie elíps aj tak o chvíľu vymažeme, len čo zistíme, aké rozmery a umiestnenie jej tvaru najlepšie vystihujú kapustu a prievozníka.

Súradnice pre kapustu by sme potrebovali posunúť ešte o niečo nižšie a aj elipsa by mohla byť o niečo menšia. Nakoniec zistíme, že elipsu bude lepšie nahradiť kruhom. Skúsme použiť kruh s rozmerom 40 bodov, ktorý posunieme o 30 bodov dole (nezabudnime hodnotu 30 prepísať aj v druhom príkaze – zišla by sa definícia konštanty; to ponechám na zváženie čitateľovi):

farba(0, 0, 100, 100);
skoč(0, -30);
kruh(40);
skoč(0, 30);

Je to lepšie. Ešte jeden pokus – nakoniec sa uspokojíme s rozmerom kruhu 45 bodov. Čím viac pokusov by sme vykonali, tým by bol náš odhad presnejší.

Java umožňuje upravovať správanie každého objektu individuálne pri jeho konštrukcii. Prejdime do hlavnej triedy na miesto, kde inicializujeme objekt kapusta:

private final Pasažier kapusta = new Pasažier("kapusta", 0, -25, 0, 25, 360, 10);

Tesne pred bodkočiarku vložme blok, do ktorého napíšeme novú verziu metódy myšV platnú len pre kapustu:

private final Pasažier kapusta = new Pasažier("kapusta", 0, -25, 0, 25, 360, 10)
{
    @Override public boolean myšV()
    {
        skoč(0, -30);
        boolean myšV = myšVKruhu(45);
        skoč(0, 30);
        return myšV;
    }
};

Návratovú hodnotu metódy myšVKruhu sme museli uložiť do pomocnej premennej, aby sme mohli vrátiť späť posunutie príkazom skoč. Ak teraz spustíme hru, ľahko si overíme, že kapusta reaguje na upravené správanie. Využili sme polymorfický mechanizmus prekrývania metód (overriding). Odteraz sa všade tam, kde je použitá metóda myšV pre kapustu, bude volať nami upravená verzia metódy.

Podobne budeme postupovať pri prievozníkovi. Keďže ten je iba jeden, môžeme metódu myšV prekryť priamo v triede Prievozník. Principiálne je to v tomto prípade (pri prievozníkovi) jedno. Ale vo všeobecnosti:

  • ak potrebujeme zmeniť správanie plošne, pre všetky inštancie určitej triedy, prekrývame v triede;
  • ak potrebujeme úpravy vykonať individuálne, pre konkrétnu inštanciu (ako to bolo pri kapuste), prekrývame pri konštrukcii.

Po niekoľkých pokusoch sme zistili, že plochu prievozníka uspokojivo pokrýva elipsa posunutá od stredu doľava o 30 bodov, vysoká 90 bodov a široká 120 bodov:

@Override public boolean myšV()
{
    skoč(-30, 0);
    boolean myšV = myšVElipse(90, 120);
    skoč(30, 0);
    return myšV;
}

Podotýkame, že naše animované objekty sa pohybujú smerom „doprava“ a „doľava“, čiže pod uhlami 0° a 180°. Obidva smery sú oproti základnému smeru robota (čo je 90° – „hore“), pootočené o 90°, takže výška a šírka elipsy sú „vymenené“. Inak povedané: „výšku“ (inak povedané „veľkosť vedľajšej poloosi“) zadávame ako prvý argument a „šírku“ (inak povedané „veľkosť hlavnej poloosi“) ako druhý argument metód robota: myšVElipse, vyplňElipsu… (Pozri dokumentáciu skupiny tried GRobot.)

Teraz môžeme kreslenie elipsy z metódy kresliTvar odstrániť. Obsluhu hry myšou realizujeme vytvorením obsluhy udalostí v konštruktore hlavnej triedy. Umiestnime do neho reakciu na kliknutie myšou a do neho umiestnime štyri jednoduché riadky riadiace animované objekty. Slovne vyjadrené – ak je kurzor myši v rámci konkrétneho objektu, nech sa pohne:

new ObsluhaUdalostí()
{
    @Override public void klik()
    {
        if (vlk.myšV()) vlk.choď();
        if (koza.myšV()) koza.choď();
        if (kapusta.myšV()) kapusta.choď();
        if (prievozník.myšV()) prievozník.prejdiRieku();
    }
};

Po spustení hry, budú všetky objekty reagovať na kliknutie ľubovoľným tlačidlom myši. Správanie hry zatiaľ nie je dokonalé. Objekty sa presúvajú niekedy nelogicky a niekedy sa pohnú viaceré naraz. To, aby sa nehýbali viaceré naraz, zabezpečíme jednoducho. Vytvoríme viacúrovňovú štruktúru if-else:

if (vlk.myšV()) vlk.choď();
else if (koza.myšV()) koza.choď();
else if (kapusta.myšV()) kapusta.choď();
else if (prievozník.myšV()) prievozník.prejdiRieku();

Po tej to úprave zareaguje na kliknutie myšou vždy len jeden objekt. Priorita je určená hierarchiou štruktúry if-else. Ďalej je potrebné zabezpečiť, aby sa objekty na palube plte hýbali súbežne s plťou a aby sa na palubu plte nemohli dostať viacerí pasažieri naraz. Najjednoduchšie riešenie je prekrytie metódy pasivita v triede AnimovanýObjekt, do ktorej vložíme jediný riadok kódu:

@Override public void pasivita()
{
    if (pozícia == Pozícia.prievozník) skočNa(prievozník);
}

Slovami vyjadrené: „ak je objekt na palube plte prievozníka, nech sa pohybuje súbežne s ním, resp. nech skočí na súradnice objektu prievozníka.“ Rýchle a účinné riešenie. Avšak keď potrebujeme využiť služby metódy vPohybe, ktorá môže spresňovať okolnosti detekcie pohybujúcich sa objektov, nemáme inú možnosť, než prekrytie metódy pracuj. Avšak, pri tom pozor! Ako je uvedené v dokumentácii robota: „prekrytím tejto metódy by sme mohli úplne prepracovať správanie robota – aktívneho aj neaktívneho, avšak odporúčame ponechať predvolené správanie a iba korigovať správanie robotov prekrývaním metód aktivita a pasivita.“ Metóda pracuj úplne determinuje správanie robota. Preto, ak ju prekrývame, musíme v jej tele zabezpečiť volanie pôvodnej metódy pracuj, inak by sme stratili funkcionalitu robota! Tento spôsob bude vyzerať takto:

@Override public void pracuj()
{
    if (!vPohybe() && pozícia == Pozícia.prievozník)
    {
        skočNa(prievozník);
    }
 
    // Zavoláme verziu metódy „pracuj“ nadradenej triedy:
    super.pracuj();
}

Nikdy nezabudnite pri prekrývaní tejto metódy volať pôvodnú verziu metódy pracuj: super.pracuj();

Druhý naznačený problém – zabránenie vstúpenia viacerých pasažierov na palubu naraz bude vyžadovať hlbšie zamyslenie. Na to, aby sme na palubu plte zabránili vstupu ďalšieho pasažiera, musíme byť najskôr schopní zistiť, či sa už na palube niekto nachádza. Vyhotovme (pre svoje potreby) požiadavku na spracovanie novej vlastnosti triedy Prievozník, ktorá bude spravovať obsah paluby:

  • Umiestnenie: trieda Prievozník;
  • Opis: vytvorenie novej vlastnosti spravujúcej obsah paluby; nech metóda na zápis buď nedovolí vykonanie ďalších príkazov, ak paluba nie je prázdna, alebo zabezpečí uvoľnenie paluby;
  • Vstupy: metóda na zápis bude prijímať objekt typu Pasažier;
  • Výstupy: metóda alebo metódy na čítanie budú umožňovať najmenej jeden z nasledujúcich spôsobov detekcie obsahu paluby: 1. vrátenie hodnoty typu boolean vyjadrujúcej (ne)prázdnosť paluby; 2. vrátenie hodnoty typu Pasažier, ktorá je v prípade prázdnej paluby rovná hodnote null;
  • Zdôvodnenie: hra vyžaduje, aby na palubu plte prievozníka mohol vstúpiť výhradne jeden pasažier; pomocou tejto vlastnosti ľahšie detegujeme, či je paluba voľná.

Požiadavku postupne naplníme. V prvom rade potrebujeme súkromnú premennú typu Pasažier. V nej budeme uchovávať aktuálneho pasažiera prítomného na palube. Ak bude jej hodnota rovná null, bude to znamenať, že paluba je prázdna. Na začiatku bude (samozrejme) paluba prázdna:

private Pasažier naPalube = null;

Premennú môžeme hneď využiť v metóde detegujúcej obsadenosť paluby. Jej realizácia bude jednoduchá. Stačí overiť, či je obsah premennej naPalube rovný null:

public boolean niektoNaPalube()
{
    return null != naPalube;
}

Uvoľnenie paluby bude tiež veľmi jednoduché. Stačí do premennej vložiť hodnotu null:

public void uvoľniPalubu()
{
    naPalube = null;
}

Kľúčové je pre nás nastupovanie pasažierov na palubu. Palubu môžeme obsadiť až po overení, či je prázdna. Prípadne lepšie: zabezpečíme, aby sa paluba pred vstupom ďalšieho pasažiera na palubu automaticky uvoľnila.

public void naPalubu(Pasažier ktorý)
{
    // <-- sem treba vložiť kód overujúci alebo zabezpečujúci „prázdnosť“ paluby
    naPalube = ktorý;
}

Keby sme chceli iba overiť, či je paluba prázdna a na základe toho ju (ne)naplniť, museli by sme vrhnúť výnimku, ktorú by sme museli zachytiť a ošetriť. Skúsme sa namiesto toho pozrieť na to, či nie sme schopní zabezpečiť uvoľnenie paluby (v prípade, že to bude situácia vyžadovať). Postupnou analýzou všetkých metód, ktoré máme k dispozícii, vyhľadáme tú, ktorá by mala vyhovieť našim požiadavkám. Metóda choď definovaná v triede Pasažier, sa správa takto:

  • ak je pasažier na brehu, nastúpi na palubu
  • a naopak, ak je na palube plte, vystúpi na najbližší breh.

Jednu z týchto akcií vykoná určite, bez obmedzujúcich podmienok. Nech ju voláme v ľubovoľnom čase. Čiže, keď metódu spustíme pre pasažiera prítomného na palube, určite palubu opustí:

if (null != naPalube) naPalube.choď();

Vďaka tomu, že si týmto správaním metódy môžeme byť istí, môžeme ho využiť v tele metódy naPalubu:

public void naPalubu(Pasažier ktorý)
{
    if (null != naPalube) naPalube.choď();
    naPalube = ktorý;
}

Predbežne budeme považovať okolie vlastnosti „práca s palubou“ za dokončené. Ak by sme potrebovali ďalšie metódy pracujúce v tomto kontexte, dokončíme ich neskôr. V tomto okamihu treba pouvažovať o tom, kde a ako tieto vlastnosti použijeme. Optimálne by bolo využiť ich vždy, keď pasažierovi (objektu) prikážeme, aby sa posunul. Keď vstupuje na palubu, treba overiť, či je plť prázdna, a keď z nej vystupuje, treba palubu uvoľniť.

Mohli by sme prekryť metódy animovaného objektu v triede Pasažier (konkrétne metódy choďNaĽavý, choďNaPravýchoďNaPrievozníka). Nič však nepokazíme ani tým, keď prekrývanie nevyužijeme a funkcionalitu doprogramujeme priamo do triedy AnimovanýObjekt. Prekrývanie je povinné v tých prípadoch, keď potrebujeme zároveň zachovať neporušenú pôvodnú funkcionalitu triedy a zároveň ju meniť v odvodených triedach.

Ak správanie uvedených troch metód upravíme, tak i keď je určené najmä pre pasažierov, nijako negatívne neovplyvní ani prievozníka. Do každej metódy umiestnime jediný podmienený príkaz. Je to bezpečné riešenie, ani výpočtová zložitosť sa príliš nezhorší. (Pri prekrytí metód by sme museli i tak volať nadradenú verziu v tele každej z nich, čo by sa násobilo počtom pasažierov. Takto budeme mať menej náročné podmienené spracovanie, ktoré bude nadbytočné iba pre jediný objekt – prievozníka.) V metódach choďNaĽavýchoďNaPravý potrebujeme zariadiť uvoľnenie paluby. To podmienime prítomnosťou postavy na palube:

public void choďNaĽavý()
{
    if (pozícia == Pozícia.prievozník)
        prievozník.uvoľniPalubu();
 
    cieľ(ľavýX, ľavýY);
    pozícia = Pozícia.ľavýBreh;
}
 
public void choďNaPravý()
{
    if (pozícia == Pozícia.prievozník)
        prievozník.uvoľniPalubu();
 
    cieľ(pravýX, pravýY);
    pozícia = Pozícia.pravýBreh;
}

Inak povedané: ak postava pri požiadavke presunutia sa na kotviacu pozíciu na brehu opúšťa palubu, automaticky uvoľňuje jej priestor, čo signalizuje volaním metódy uvoľniPalubu.

Naproti tomu v metóde choďNaPrievozníka potrebujeme zistiť, či je paluba voľná a následne do príslušnej premennej vložiť odkaz na aktuálneho pasažiera. Pri našom riešení obsah paluby automaticky vyprázdňujeme, ak treba. Pred chvíľou sme si pripravili metódu schopnú priestor paluby uvoľniť. Využijeme ju (môžeme to však vykonať len pre objekty, ktoré sú inštanciami triedy Pasažier):

public void choďNaPrievozníka()
{
    if (this instanceof Pasažier)
        prievozník().naPalubu((Pasažier)this);
 
    cieľ(cieľ = prievozník);
    pozícia = Pozícia.prievozník;
}

Teraz by sa nikdy nemali na palube vyskytnúť dvaja pasažieri naraz. Bolo by to v poriadku, keby bol program preložiteľný. Preklad programu sa zastaví v tele metódy choďNaĽavý. Chybové hlásenie pri príkaze prievozník.uvoľniPalubu(); oznamuje, že metódu uvoľniPalubu nie je možné nájsť. Skutočne, metódu sme definovali v triede Prievozník, ale inštancia prievozník v triede AnimovanýObjekt je typu AnimovanýObjekt, nie Prievozník. V čase, keď sme prievozníka definovali, sme nemali na výber. Museli sme využiť jestvujúce triedy. Teraz, keď trieda Prievozník fyzicky jestvuje, môžeme zmeniť údajový typ prievozníka na taký, aký mu prináleží:

private static Prievozník prievozník;
 
public static void prievozník(Prievozník prievozník)
{
    AnimovanýObjekt.prievozník = prievozník;
}
 
public static Prievozník prievozník()
{
    return prievozník;
}

Po tejto zmene je program preložiteľný bez chýb a zdá sa, že funguje, ako má. Menší problém nastáva, keď náhodou pošleme na palubu plte dvoch pasažierov v rýchlom slede za sebou (tak, aby boli obaja ešte v pohybe). Prvý pasažier sa síce na chvíľu „zatvári“, že sa vracia späť na svoje miesto, no vzápätí sa obráti späť na palubu, a tam zostane stáť. Prievozník ho však na druhú stranu neprevezie. Zdá sa, že objekt „si len myslí“, že je na palube plte.

Je síce malá šanca, že hráč bude postupovať takto a ide iba o vizuálny problém, ale nemali by sme nič zanedbávať. Čo je koreňom problému? Je to rovnaký problém, aký sme pred časom riešili prekrytím metódy dosiahnutieCieľa. Metódy choďNaĽavýchoďNaPravý síce pošlú pasažiera na breh (implicitne predpokladajúc, že je na palube plte), ale nijako neoverujú ani nezabezpečia zrušenie sledovania vnútorného cieľa (pre prípad, že by bol pasažier ešte len na ceste na plť). Riešenie opäť nie je žiadnou veľkou záhadou. Stačí do oboch metód pridať riadok kódu, ktorým zrušíme vnútorný cieľ animovaného objektu:

cieľ = null;

Tým sme uzavreli kapitolu ovládania. Z hľadiska funkčnosti hry nám zostáva vyriešiť jediné: detegovať, situácie vzájomného požierania sa pasažierov a situáciu úspešného dokončenia hry. Z vizuálneho hľadiska chýba doriešenie vykresľovania grafiky prostredia. Oboma záležitosťami sa budeme zaoberať v nasledujúcej kapitole. Rovnako pridáme aj jednoduché ozvučenie postáv. Potrebné súbory sme si pripravili v úvodných kapitolách grafickej časti.

Príloha 11 – ďalšie verzie tried AnimovanýObjekt, Prievozník a HlavnáTrieda

Zobraziť | Prevziať